npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@bergetai/odoo-cli

v0.12.1

Published

Agent-first CLI for the Odoo External API

Readme

odoo-cli

Agent-first CLI for the Odoo External API.

Every command is non-interactive, scriptable, and supports --json for stable machine-readable output.

Installation

npm install -g @bergetai/odoo-cli

Requires Node.js 20 or later.

Usage

# Authenticate (API key via stdin — never as a flag)
pass odoo/key | odoo auth login --profile production --url https://acme.odoo.com --db acme --username automation

# Authenticate a second environment (kept side by side in one config)
pass odoo/stage-key | odoo auth login --profile stage --url https://odoo.stage.berget.ai --db berget-stage --username automation

# Always select the profile explicitly — commands never guess credentials
odoo --profile production partners list --company --json
ODOO_PROFILE=stage odoo partners list --company --json

# Check credentials and their configuration sources
odoo auth status --profile production --json

# Unauthenticated connectivity check
odoo version

# Partners (companies and contacts — res.partner)
odoo partners list --company --json
odoo partners list --search "acme" --tag "Partner" --json   # ilike on name/email, tag filter
odoo partners show 51
odoo partners tags                                          # tags with usage counts
odoo partners create --name "Acme Corp" --is-company true --email [email protected]
odoo partners update 51 --email [email protected] --dry-run
odoo partners update 51 --add-tag "AI" --remove-tag "Draft" --dry-run
odoo partners delete 54 --dry-run

# Generic model introspection (any model with an XML-RPC API)
odoo models fields res.partner --filter "^email" --json
odoo models fields crm.lead --custom              # only x_* custom fields
odoo models read res.partner --ids 51,52 --fields name,city
odoo models read crm.lead --search "microsoft" --json  # ilike on name (max 100)

# List and inspect records
odoo crm leads list --open --limit 20
odoo crm leads show 42
odoo helpdesk tickets list --team Support --json
odoo project list --json
odoo project tasks list --project "Content Pipeline" --open
odoo project tasks show 850
odoo documents tree --count
odoo documents records list --folder "Product Backlog" --limit 20
odoo documents records list --folder "Projects > Product Backlog"  # nested path
odoo documents records show 25
odoo documents records download 25 --out ./spec.pdf

# Mutations (support --dry-run)
odoo crm leads create --name "New lead" --email [email protected]
odoo crm leads update 42 --stage Won
odoo crm leads delete 42 --yes

# Sales teams (crm.team)
odoo crm teams list
odoo crm teams show 5
odoo crm teams create --name "Partnerships" --team-leader [email protected]
odoo crm teams update 5 --name "Direct Sales (renamed)" --dry-run
odoo crm teams delete 5 --dry-run

# Record attachments (ir.attachment) — explicit scope required
odoo attachments list --res-model helpdesk.ticket --res-id 398 --json
odoo attachments list --res-model mail.message --res-id 253924
odoo attachments list --search "spec" --limit 100
odoo attachments show 31337
odoo attachments download 31337 --out ./spec.pdf
odoo attachments delete 31337 --dry-run
odoo attachments delete 31337

# Marketing (mailing.*) — email is the key; trace deletion is always explicit
odoo marketing lists list
odoo marketing lists show 11
odoo marketing contacts list --email [email protected] --json
odoo marketing contacts list --list "Berget Code waitlist"
odoo marketing contacts delete 441 --dry-run
odoo marketing contacts delete 441
odoo marketing traces list --email [email protected] --json
odoo marketing traces delete 7001 --dry-run
odoo marketing traces delete 7001

# Sign-in identities (res.users) — show + guarded delete only
odoo users show 9
odoo users delete 9 --dry-run
odoo users delete 9
odoo users delete 9 3782 --json

# Footprint (read-only) — everything tied to an email, with SOP dispositions
odoo footprint [email protected] --json
odoo footprint [email protected]                  # grouped human report
odoo footprint [email protected] --active-only    # what the UI sees

# Internal notes (NEVER sends email to customer)
odoo helpdesk tickets add-note 398 --body "Internal note text"
odoo helpdesk tickets add-note 398 --body-file note.md
odoo crm records add-note 338 --body "Customer replied: ..." --author 40

# Log note correction (message-update / message-delete; comment-type only)
odoo crm records message-update 270799 --body-file fixed.html --dry-run
odoo crm records message-update 270799 --body-file fixed.html
odoo crm records message-delete 253924 258332 --dry-run
odoo crm records message-delete 253924 258332

Configuration precedence: flags > environment (ODOO_URL, ODOO_DB, ODOO_USERNAME, ODOO_API_KEY) > the selected profile in $XDG_CONFIG_HOME/odoo/config.toml. The API key is never accepted as a flag value.

Profiles

Credentials are stored per profile — one TOML table per environment:

[profiles.production]
url = "https://odoo.berget.ai"
db = "berget"
username = "automation"
api_key = "…"
server_version = "18.0+e"

[profiles.stage]
url = "https://odoo.stage.berget.ai"
db = "berget-stage"
username = "automation"
api_key = "…"

Every command requires an explicit profile: pass --profile <name> or set ODOO_PROFILE. Without one the command exits 2 — credentials are never guessed, so a command can never hit the wrong environment by accident. auth login and the flags-only odoo version check tolerate a profile that does not exist yet (bootstrap), while any other command reports an unknown profile.

Older configs (config.json, including top-level credential keys) keep working: they read as the default profile and migrate to config.toml on the next auth login — the legacy file is removed once the TOML config holds everything.

Note: odoo --version prints the CLI's own version; odoo version queries the server.

Features

  • Footprint overview (footprint <email>) — one read-only invocation that enumerates everything tied to an email across the erasure-relevant modules (res.partner incl. archived, res.users, crm.lead, mailing.*, helpdesk.ticket, mail.message, ir.attachment), correlates the found partners into the RESTRICT accounting surfaces (account.move/sale.order incl. invoice & shipping addresses, account.payment), and classifies every record by erasure disposition: delete, anonymize (referenced — shell, never unlink), keep (bokföringslagen 7-year retention), review (account roots, legal-claim branch, internal users, unverifiable attachment parents). --json emits a structured per-group payload (fixed 11-group key order, totalRecords, per-group found/total server count/truncated/dispositions tally, many2one tuples normalized to {id, name}) with a commandHint on each actionable record, a top-level runOrder (SOP execution order), and decision-grade fields on partner records (child_ids/user_ids/vat). Review records with out-of-footprint parents are resolved inline — the parent's name is embedded in the reason (bounded by a per-model fetch cap; beyond it records keep an inspectHint). Every record tells you what to run next — or already answers the question. Read-only by design: verification (art. 12.6) is a human step and the argument must already be the verified email; record-rule hidden rows (e.g. user_notification messages) mean the raw SQL pre-flight stays the authoritative evidence. Never mutates anything
  • Read-only CRM surface (crm leads list/show) — default columns, --fields/--all-fields, --limit/--offset/--order, --domain/--filter with fields_get type checking, reference resolution (--stage/--team/--assignee/--tag), table/JSON output with search_count envelope
  • Partners surface (partners list/show/tags/create/update/delete) — res.partner CRUD with --company/--individual filters, --search (case-insensitive name/email match), --tag (repeatable, resolves res.partner.category by exact name then ilike), --archived (include inactive), curated list columns (ID, Name, Company Type, City, Email, Is a Company, Tags, Salesperson), curated show view with tag names, salesperson, customer/supplier ranks plus every populated x_studio* custom field, partners tags (usage counts via read_group, sorted by usage), first-class mutation flags for --name, --email, --phone, --website, --parent (m2o), --is-company, --street, --city, --zip, --country (m2o), --comment, --add-tag/--remove-tag on update (unknown tags fail on add; removal silently skips tags the record lacks; protected fields company_type and x_studio_invoice_approved refuse writes), full --dry-run support, --fields/--all-fields, table/JSON output
  • Sales teams surface (crm teams list/show/create/update/delete) — --archived (include inactive teams), --team-leader (team leader, login/email/name), --active/--use-leads/--use-opportunities/--invoiced-target/--color/--sequence, full --dry-run support, --fields/--all-fields, table/JSON output
  • Read-only Helpdesk surface (helpdesk tickets list/show) — --open/--closed via stage fold, strict team-scoped stage resolution, module-not-installed error (exit 4)
  • Read-only Project surface (project list/show) — --user (project manager) and --tag filters, archived projects excluded by default, module-not-installed error (exit 4)
  • Read-only Project task surface (project tasks list/show) — --project (required on list, resolved by name or id), project-scoped --stage, --assignee (many2many user_ids), --tag, --priority (0/Low, 1/High), --open/--closed via the task's is_closed flag, --domain/--filter, table/JSON output with search_count envelope, module-not-installed error (exit 4)
  • Generic model surface (models fields/read) — introspect and read any model exposed through execute_kw: fields lists fields_get metadata (type, label, selection, relation, required, store) with --filter <regex> (case-insensitive on field name or label) and --custom (only x_* fields); read fetches up to 100 records by --ids 1,2,3 or a name ilike --search, with --fields a,b,c (default: name). Unknown models/fields use the standard error taxonomy
  • Documents surface (documents) — tree (hierarchical folder listing with --count/--depth/--folder), records list/show (folders excluded by default, --type folder|binary|url|all, --folder/--owner/--partner/--tag), folders list, records create (folder via --folder-type folder, link via --link <url>), records update (move via --folder), records delete, upload (local file → base64 datas, MIME guessed from extension, --dry-run previews without sending bytes), and records download <id> (binary document → local file, --out <path>/--force, --out - streams to stdout). Everywhere a --folder is accepted, folders resolve by name, numeric id, or nested 'Parent > Child' path (segments resolved within their parent, which disambiguates repeated folder names). Folders are documents.document rows with type='folder'; the hierarchy is folder_id. Module-not-installed error (exit 4)
  • Writes (create, update, delete) — single-call atomic batch write, client-side no-op diffing, delete convergence with skipped ids, post-failure "record may have been created" hint on create
  • --dry-run for all mutations — full validation, resolution, and before/after diffs without mutating
  • Internal notes (add-note) — creates internal Log Notes only (subtype_id=2, message_type=comment), NEVER sends email to customer, by design prevents accidental customer communication. Optional --author <email|name|id> attributes the note to the partner who actually sent the logged message (resolved via res.partner; ambiguous matches list candidates and never guess)
  • Log note correction (message-update, message-delete) — repair formatting or remove duplicate log notes; strictly limited to comment-type messages (emails/notifications refused by design), full --dry-run support, unknown delete ids skipped
  • Config profiles (named environments in one 0600 config file, explicit --profile/ODOO_PROFILE selection), atomic config writes with legacy migration, XML-RPC transport with timeouts and HTTPS enforcement, and an auth failure taxonomy (db missing / bad credentials / unreachable)

Compatibility

Validated against a real Odoo 18.0+e instance. Schema realities discovered against the live server:

  • helpdesk.ticket.ticket_type_id may be absent (edition/version-dependent). Curated show fields are intersected with fields_get, so absent optional fields degrade gracefully instead of faulting the read.
  • helpdesk.stage links to teams via team_ids (many2many, shared stages) on some instances rather than team_id (many2one). Stage scoping introspects the stage model's schema and uses whichever field exists.
  • execute_kw kwargs must be a separate trailing dict param; packing it into the positional-args array makes the server silently ignore it (this caused fields_get to return full/empty structs).
  • project.task has no user_id field; assignees are the many2many user_ids. The priority selection is two-valued (0/Low, 1/High), not the four-level scale used by CRM/Helpdesk. Tasks carry a direct is_closed boolean, which --open/--closed filter on (authoritative, unlike stage fold). Stages (project.task.type) link to projects via the many2many project_ids, so --stage resolves within the --project.

Development

npm install
npm run build   # compile to dist/
npm test        # build + unit/e2e tests against a fake XML-RPC server

Contributing

Issues and pull requests are welcome at github.com/berget-ai/odoo-cli.

License

MIT