<p align="center">
  <a href="https://www.wanasapps.com">
    <img src="https://cdn.jsdelivr.net/npm/wanas-zone-cli/WanasApps-logo-color.svg" alt="Wanas Apps" width="300">
  </a>
</p>

<h1 align="center">Zoho Zone &nbsp;·&nbsp; <code>zone</code></h1>

<p align="center">
  <a href="https://www.npmjs.com/package/wanas-zone-cli"><img src="https://img.shields.io/npm/v/wanas-zone-cli?style=for-the-badge&logo=npm&color=CB3837&label=npm" alt="npm version"></a>
  <a href="https://www.npmjs.com/package/wanas-zone-cli"><img src="https://img.shields.io/npm/dt/wanas-zone-cli?style=for-the-badge&color=28A745&label=downloads" alt="npm downloads"></a>
  <img src="https://img.shields.io/node/v/wanas-zone-cli?style=for-the-badge&logo=node.js&logoColor=white&color=339933&label=node" alt="node version">
  <img src="https://img.shields.io/npm/l/wanas-zone-cli?style=for-the-badge&color=512BD4&label=license" alt="license">
</p>

**One CLI for the whole Zoho suite.** `zone` signs you in once and gives you **9,909 typed commands across 64 services** — CRM, Books, Desk, Mail, Projects, Inventory, People, WorkDrive, Cliq, Campaigns, Creator, Catalyst, Zoho One, Payments, Contracts, Connect and the rest — plus a raw proxy for any endpoint the typed commands miss. Auth, token refresh, datacenter routing, org/portal context and pagination are handled once, in one place. It is built for coding agents as much as for people: data on stdout, messages on stderr, a JSON error envelope, meaningful exit codes, a dry-run switch and a policy gate that make it safe to hand to an AI tool pointed at a live client org.

```bash
npm install -g wanas-zone-cli
```

## Contents

[Install and first call](#install-and-first-call) · [The model](#the-model) · [Agent contract](#agent-contract) · [Safety](#safety) · [Skills for AI tools](#skills-for-ai-tools) · [Finding a command](#finding-a-command) · [Services](#services) · [Authentication](#authentication) · [Environment variables](#environment-variables) · [Troubleshooting](#troubleshooting) · [Development](#development) · [Changelog](CHANGELOG.md) · [Support](#support)

## Install and first call

```bash
npm install -g wanas-zone-cli
zone login crm                 # one browser consent; add more services any time
```

The first real call — a COQL count against your own CRM:

```bash
zone crm coql "select COUNT(id) as leads from Leads where id is not null" --json
```

```json
{
  "data": [
    {
      "leads": 4424
    }
  ],
  "info": {
    "count": 1,
    "more_records": false
  }
}
```

**If an AI coding tool will drive zone, install the skills before you ask it for Zoho work.** No model knows 9,909 commands from memory; the skills carry the real names, required flags, payload shapes and the traps found on live orgs. Install the skills BEFORE the first prompt — it is one command:

```bash
zone skill                     # this project (interactive picker for the agent)
zone skill --global            # once, for every project on this machine
```

Prefer not to install globally? `npx wanas-zone-cli <command>` works too.

## The model

Five ideas cover almost everything `zone` does.

**One login, many services.** `zone login crm books desk` opens one consent for the named services (`--all` takes every core service in one go; extended services are named explicitly). Zoho issues one shared grant; each service gets its own session file under `.zone/sessions/`, so a token problem in one never touches another. Refresh is automatic.

**Sessions are per project.** `zone login` writes to `./.zone`, found the way git finds `.git` — by walking up from the current directory. Each client repo carries its own Zoho identity and `cd` switches org with no re-login. `--global` uses `~/.zone`; `ZONE_HOME` overrides both. `.zone` directories self-ignore in git.

**Typed commands, with a proxy underneath.** Every command has the shape `zone <service> <group> <name> [args] [flags]`:

```bash
zone crm record list Leads --fields Last_Name,Company --per-page 5
zone books invoice list --per-page 5 --toon
zone mail folder list --toon
zone crm coql "select id, Deal_Name from Deals where Stage = 'Closed Won'"
```

Anything not typed yet is one call away through the same client, with the same auth, refresh, datacenter routing and 401 retry:

```bash
zone api crm GET /Leads --query fields=Last_Name,Company --query per_page=5
zone api crm GET /org --out org.json
echo '{"data":[{"Last_Name":"Doe"}]}' | zone api crm POST /Leads --data - --dry-run
```

`--data` takes inline JSON, `@file`, or `-` for stdin; `--form k=v` sends urlencoded fields; `--out` saves raw bytes.

**Context is injected for you.** Zoho scopes calls inconsistently — Books wants `organization_id` in the query, Desk an `orgId` header, Projects a portal id in the path. Store each once and every later call carries it; an explicit `--query organization_id=...` always wins.

```bash
zone ctx books organization_id=847058403
zone ctx desk orgId=700123456
zone ctx books                          # show what is stored
```

**CRM sandbox and developer edition.** Zoho CRM serves the same API on three hosts — production (`www.zohoapis.<dc>`), the org's sandbox (`sandbox.zohoapis.<dc>`) and the developer edition (`developer.zohoapis.<dc>`) — and binds every OAuth token to the organization picked on the consent screen. Log in to the sandbox in its own store (`ZONE_HOME=./.zone-sandbox zone login crm`, choose the Sandbox organization): zone asks the org which environment it is (`GET /org` → `type`), stores `env=sandbox`, and every `zone crm …` command, `zone api crm`, bulk, functions and the absolute-URL rows go to the sandbox host. `zone status` grows an **ENV** column while any service is off production (and shows `SANDBOX ≠ token production` when the context and the token disagree), each command prints one stderr line naming the environment, and `--dry-run` reports `environment` and `base`. `zone ctx crm env=…` moves the host by hand and warns when the stored token is bound elsewhere; a production token on the sandbox host gets `DOMAIN_TOKEN_MISMATCH` with the fix in its hint.

**Output is a contract.** Data goes to stdout, messages to stderr, so a pipe is never polluted. `--json` renders JSON; `--toon` renders TOON, a compact form that costs an LLM 30–60% fewer tokens for the same data. Both wrap plain-text responses so `JSON.parse(stdout)` always works. The rest of that contract is the next section.

## Agent contract

`zone`'s primary users are coding agents. What they can rely on:

- **stdout is data, stderr is messages.** Progress, hints and warnings never land in the data stream; `-q` silences them.
- **Exit codes mean something.**

  | Code | Meaning | What a caller should do |
  |--:|---|---|
  | `0` | success | parse stdout |
  | `1` | unexpected error | report it (`zone bug`) |
  | `2` | usage error — bad flags, bad input, missing argument | fix the command; `--help` has the shape |
  | `3` | not authenticated | a human must run `zone login` |
  | `4` | missing OAuth scope | re-login that service |
  | `5` | Zoho rejected the request (4xx) | read the `zoho` body; do not retry blindly |
  | `6` | blocked by policy | report it; the rule that blocked is named |
  | `7` | retryable — network, 429, 5xx | back off and retry |

- **The last stderr line is JSON on failure** when `--json` or `--toon` is set (or `ZONE_JSON_ERRORS=1`): `{"ok":false,"error":{code,message,hint,http_status,zoho,context}}`. Commander's own usage errors are included, so a missing argument parses the same way a Zoho error does:

  ```bash
  zone --json crm record get
  ```

  ```
  {"ok":false,"error":{"code":"BAD_INPUT","message":"missing required argument 'module'","hint":"Run \"zone --help\" or \"zone <command> --help\" for the exact arguments and flags.","http_status":null,"zoho":null,"context":null}}
  ```

- **Help is machine-readable too.** `zone crm record create --help --json` returns the arguments (with `required`), options and subcommands as a JSON shape, so an agent can learn a command without parsing prose.
- **`hint` is the fix.** Every structured error that has a known remedy names it — the missing flag, the `zone ctx` line to run, the `zone login` to redo.
- **API versions fall back on their own.** Zoho serves several CRM API versions side by side and they drift: a path dropped from v9 still answers on v8, and a settings config that 500s on v9 and v8 reads in full on v7. When a call fails with a drift signature (`404 INVALID_URL_PATTERN`, "deprecated in this version", or a `500 INTERNAL_ERROR` on a read), zone walks v9 → v8 → v7 → … → v2 until one answers, never below a row's minimum version, and prints one line naming the version that served it with the `--api-version` pin to make it permanent. Writes are only replayed for the 404 case (nothing ran). An explicit `--api-version` or `ZONE_API_FALLBACK=0` switches it off; if every version fails, the error carries `versions_tried` and says so.
- **Throttling is handled the way Zoho documents it.** CRM meters API credits over a rolling 24 hours plus a concurrency cap; the finance apps allow 100 requests a minute per organization plus a daily cap; all of it arrives as HTTP 429. zone waits out `Retry-After` or the rate-limit reset header (else exponential backoff) and retries up to `ZONE_RETRIES` times, each wait capped at `ZONE_RETRY_MAX_WAIT_MS`; a reset hours away, or Books' daily-limit code 45, fails fast instead. Connection failures and 5xx are retried for reads only — a write may already have been applied. Exit 7 means the limit did not clear: the error carries `rate_limit.reset_at`, and `zone status` shows the quota each service last reported.

## Safety

zone is the tool an agent calls to reach live client orgs, so the safety properties a harness would normally provide have to live here — a harness granting `shell(zone)` has granted every write to every authenticated service and cannot express "reads only".

**`--dry-run` works on every command.** A mutating request is resolved and printed instead of sent, exit 0. Reads still run, so an agent can leave it on while building a call. `zone bug` honours it too.

```bash
zone crm record delete Leads 123 --dry-run --json
```

```json
{
  "dry_run": true,
  "service": "crm",
  "method": "DELETE",
  "path": "/Leads/123",
  "query": {},
  "body": null,
  "api_version": null
}
```

**Policy gate.** `--allow` / `--deny` (or `ZONE_POLICY` / `ZONE_POLICY_DENY`) match `service:METHOD` with `*` on either side. Deny always wins; no policy means no restriction. A blocked call exits `6` and names the rule. Classification is by what an endpoint *does*, not its verb: COQL and bulk read are POSTs that count as reads, and the Campaigns / Marketing Automation writes that Zoho serves over GET count as writes — so `*:GET` really is read-only.

```bash
ZONE_POLICY='*:GET' zone crm record create Leads --data '{"data":[{"Last_Name":"x"}]}' --json
```

```
error: Blocked by policy: crm:POST /Leads — not in the allow list.
  hint: Add "crm:POST" (or "crm:*") to the allow list to permit this.
{"ok":false,"error":{"code":"POLICY_DENIED","message":"Blocked by policy: crm:POST /Leads — not in the allow list.","hint":"Add \"crm:POST\" (or \"crm:*\") to the allow list to permit this.","http_status":null,"zoho":null,"context":null}}
```

**Secret redaction, on by default.** Fields named like credentials (`access_token`, `client_secret`, `password`, `api_key`, …) are masked on every path out — stdout, `--verbose` details and the machine error line alike — and the literal value of any `ZONE_*` secret in the environment is scrubbed from free text. `zone token` is exempt because emitting the token is the command; `zone vault` takes `--reveal`, refused under `--json`/`--toon` or off a TTY unless `ZONE_REVEAL_UNSAFE=1`.

**Audit log.** `--audit <file>` (or `ZONE_AUDIT`) appends one JSONL line per request — real, denied or dry-run — with the method, path, effect (`read`/`write`), status and timing. Metadata only, never a body. A write failure warns once and never fails the command.

```json
{"ts":"2026-09-04T16:35:54.367Z","service":"crm","method":"DELETE","path":"/Leads/1","effect":"write","status":null,"outcome":"denied","policy":"denied","dry_run":false,"ms":null,"pid":40992}
```

**One seam, no bypasses.** Every request — declarative rows, the `zone api` proxy, every stateful handler, every file upload and download — goes through `src/core/apiClient.js`. A test fails if any raw HTTP call in the tree sits outside that gate. The same seam refuses `.`/`..` path segments, refuses to attach a token to a non-Zoho host, and treats an HTML page where JSON was expected as an error rather than a result.

## Skills for AI tools

`zone skill` writes skill files where your agent already looks — no plugin, no MCP server, no config. Each per-service skill carries that service's real commands, the flags Zoho requires, where a `--data` body goes, base URL, context ids and quirks, generated from the installed CLI; 21 services add a hand-written guide of live-org behaviour (CRM, Books, Inventory, Desk, Projects, Mail, WorkDrive, Sheet, Writer, Meetings, Social, People, Sprints, Invoice, Billing, Analytics, Cliq, SalesIQ, Partner, Commerce, Payroll). Only a skill's one-line description is resident on each prompt; the body loads when it triggers, so an agent pays for the service it is using rather than for all 64.

```bash
zone skill --ide claude             # all skills into .claude/skills/  (add --global for every project)
zone skill crm --ide cursor         # one service as .cursor/rules/zoho-crm.mdc
zone skill deluge --ide codex       # a marked section in AGENTS.md — never overwrites your own text
zone skill --stdout                 # print instead of writing (pipe into any agent config)
```

| Agent / IDE | `--ide` | Project file | `--global` file |
|---|---|---|---|
| Claude Code | `claude` | `.claude/skills/<skill>/SKILL.md` | `~/.claude/skills/…` |
| Antigravity / Gemini Agent | `antigravity` | `.agents/skills/<skill>/SKILL.md` | `~/.agents/skills/…` |
| Cursor | `cursor` | `.cursor/rules/<skill>.mdc` | — (paste into Settings › Rules) |
| Windsurf / Cascade | `windsurf` | `.windsurfrules` | `~/.codeium/windsurf/memories/global_rules.md` |
| GitHub Copilot | `copilot` | `.github/copilot-instructions.md` | — (per repository) |
| Cline / Roo Code | `cline` | `.clinerules` | `~/Documents/Cline/Rules/zoho-zone.md` |
| Gemini CLI | `gemini` | `GEMINI.md` | `~/.gemini/GEMINI.md` |
| Codex / any AGENTS.md tool | `codex` | `AGENTS.md` | `~/.codex/AGENTS.md` |
| Anything else | `markdown` | `ZOHO_ZONE_AI_CONTEXT.md` | `~/ZOHO_ZONE_AI_CONTEXT.md` |

Per-skill targets (Claude, Antigravity, Cursor) get one file per topic: the router, the Deluge language guide with its full reference, one per service, the delivery workflow, accounting safety, and the CRM widget subtypes. Shared-file targets (`AGENTS.md`, `GEMINI.md`, `.clinerules`, `.windsurfrules`, Copilot) have no trigger mechanism, so there `zone skill` adds the router and Deluge as marked sections and a service is added when you name it (`zone skill crm --ide codex`). Sections are rewritten in place between their own begin/end markers; text you wrote is never touched, and a per-skill file you wrote yourself is refused without `--force`. Re-run `zone skill` after upgrading zone.

For agents without file-based rules, `zone llm` prints the same guide to stdout (`zone llm crm` for one service, `zone llm core` for the router only).

## Finding a command

You never have to leave the terminal, and you do not need an inventory:

```bash
zone services                      # all 64 services, auth state, doc links
zone crm --help                    # a service's command groups
zone crm blueprint --help          # a group's commands
zone crm blueprint create --help   # arguments, flags and notes for one command (add --json for a machine shape)
zone help books                    # service card: scopes, base URL, context, every group and leaf, quirks
zone llm crm                       # the whole typed inventory for one service, agent-readable
```

The full list of all 9,909 commands is generated into [`docs/COMMANDS.md`](docs/COMMANDS.md) and, with method, path and scope per command plus live search, into `docs/zone-commands.html` (`npm run gen:docs`).

## Services

64 services. **Extended** ones carry newer, larger scope sets and are excluded from `zone login --all` — name them explicitly (`zone login desk analytics`). The last column lists the first command groups; `zone help <key>` shows them all.

| Service | Key | Commands | Groups |
|---|---|--:|---|
| Zoho CRM | `crm` | 982 | record, coql, whoami, module, field, layout, … (109 groups) |
| Zoho Mail | `mail` | 229 | account, folder, label, mail, attach, thread, … (13 groups) |
| Zoho Meeting | `meetings` | 41 | org, meeting, webinar, recording, poll, report, … (9 groups) |
| Zoho Books | `books` | 945 | org, contact, contactperson, estimate, salesorder, invoice, … (59 groups) |
| Zoho Inventory | `inventory` | 598 | orgs, contact, contactperson, item, group, composite, … (51 groups) |
| Zoho Creator | `creator` | 29 | meta, record, bulk, fn, pull, publish |
| Zoho Partner Store | `partner` | 15 | leads, subscriptions, transactions, commissions, pull |
| Zoho Social | `social` | 36 | post, portal, brand, channel, posts, media, … (13 groups) |
| Zoho Projects | `projects` | 513 | portal, project, tasklist, task, timelog, event, … (52 groups) |
| Zoho Desk | `desk` | 969 | ticket, thread, ticket-comment, ticket-attach, ticket-time, ticket-timer, … (111 groups) |
| Zoho Sign | `sign` | 36 | request, template, folder, fieldtype, doctype, signgroup |
| Zoho Writer | `writer` | 47 | document, template, merge, bulk, combine, sign, … (8 groups) |
| Zoho Sheet | `sheet` | 130 | workbook, worksheet, records, table, chart, pivot, … (17 groups) |
| Zoho Learn | `learn` | 63 | course, lesson, hub, space, manual, article, … (10 groups) |
| TrainerCentral | `training` | 24 | course, chapter, lesson, material, assignment, learner, … (8 groups) |
| Zoho People | `people` | 447 | record, form, employee, leave, attendance, timetracker, … (52 groups) |
| Zoho Campaigns | `campaigns` | 91 | list, topic, campaign, coupon, contact, segment, … (18 groups) |
| Zoho Marketing Automation | `marketing` | 42 | list, campaign, coupon, lead, segment, tag, … (9 groups) |
| Zoho WorkDrive | `workdrive` | 165 | file, folder, workspace, team, user, share, … (32 groups) |
| Zoho Cliq | `cliq` | 159 | channel, chat, message, user, bot, buddy, … (25 groups) |
| Zoho Survey | `survey` | 10 | user, portal, survey, collector, distribution, webhook |
| Zoho Analytics *(extended)* | `analytics` | 182 | org, workspace, view, data, row, table, … (28 groups) |
| Zoho Bigin *(extended)* | `bigin` | 76 | record, note, user, org, meta, pipeline, … (16 groups) |
| Zoho Recruit *(extended)* | `recruit` | 78 | record, candidate, attachment, meta, user, interview, … (18 groups) |
| Zoho Billing *(extended)* | `billing` | 277 | org, customer, subscription, plan, product, addon, … (28 groups) |
| Zoho Invoice *(extended)* | `invoice` | 268 | org, contact, invoice, estimate, payment, creditnote, … (21 groups) |
| Zoho Expense *(extended)* | `expense` | 146 | expense, report, trip, category, user, setting, … (11 groups) |
| Zoho Sprints *(extended)* | `sprints` | 214 | team, project, sprint, item, epic, release, … (36 groups) |
| Zoho Bookings *(extended)* | `bookings` | 29 | workspace, service, staff, resource, availability, appointment, … (8 groups) |
| Zoho FSM *(extended)* | `fsm` | 155 | work-order, appointment, request, estimate, contact, work-type, … (30 groups) |
| Zoho SalesIQ *(extended)* | `salesiq` | 186 | portal, operator, department, brand, conversation, call, … (30 groups) |
| Zoho Vault *(extended)* | `vault` | 35 | secret, share, chamber, user, usergroup, audit, … (7 groups) |
| Zoho Calendar *(extended)* | `calendar` | 61 | calendar, event, settings, group, notification, activity, … (13 groups) |
| Zoho Contacts *(extended)* | `contacts` | 19 | contact, category |
| Zoho Tables *(extended)* | `tables` | 40 | portal, workspace, base, table, view, field, … (12 groups) |
| Zoho Commerce *(extended)* | `commerce` | 178 | store, product, order, webhook, page, import, … (35 groups) |
| Zoho Payroll *(extended)* | `payroll` | 133 | org, employee, salary, statutory, tax, payrun, … (19 groups) |
| Zoho Catalyst *(extended)* | `catalyst` | 104 | project, zcql, table, row, bulk, fn, … (22 groups) |
| Zoho Assist *(extended)* | `assist` | 18 | user, session, unattended, report |
| Zoho Lens *(extended)* | `lens` | 27 | user, session, report, schedule, org, department, … (9 groups) |
| Qntrl *(extended)* | `qntrl` | 233 | user, org, card, workspace, circuit, function, … (36 groups) |
| Zoho Voice *(extended)* | `voice` | 53 | user, log, agent, queue, powerdialer, number, … (14 groups) |
| ZeptoMail *(extended)* | `zeptomail` | 35 | domain, agent, template, suppression, email, file, … (10 groups) |
| Zoho Backstage *(extended)* | `backstage` | 80 | event, attendee, member, sponsor, portal, event-clone, … (22 groups) |
| Zoho One *(extended)* | `one` | 31 | org, field, location, designation, user, group, … (8 groups) |
| Zoho Directory *(extended)* | `directory` | 30 | geo, org, field, location, designation, user, … (8 groups) |
| Zoho Payments *(extended)* | `payments` | 55 | customer, payment-method-session, payment-method, payment-session, terminal-session, mandate, … (16 groups) |
| Zoho Webinar *(extended)* | `webinar` | 32 | org, webinar, recording, poll, report, user, … (9 groups) |
| Zoho Connect *(extended)* | `connect` | 279 | network, profile, user-follow, favourite, block, activity, … (43 groups) |
| Zoho Contracts *(extended)* | `contracts` | 86 | org, user, department, counterparty-type, counterparty, counterparty-contact, … (15 groups) |
| Zoho Shifts *(extended)* | `shifts` | 45 | org, employee, access-level, skill, shift, timesheet, … (12 groups) |
| Zoho Workerly *(extended)* | `workerly` | 18 | meta, record, timelog |
| Zoho Thrive *(extended)* | `thrive` | 2 | purchase, activity |
| Zoho POS *(extended)* | `pos` | 210 | org, contact, contact-person, item-group, item, composite-item, … (22 groups) |
| Zoho PageSense *(extended)* | `pagesense` | 20 | experiment, goal, audience, report, projectgoal, customevent |
| Zoho BugTracker *(extended)* | `bugtracker` | 86 | portal, project, activity, status, milestone, timelog, … (17 groups) |
| Zoho DataPrep *(extended)* | `dataprep` | 43 | org, workspace, pipeline, connection, datasource, stage, … (12 groups) |
| Zoho Apptics *(extended)* | `apptics` | 16 | project, crash, event, screen, device |
| Zoho IoT *(extended)* | `iot` | 78 | product, product-gallery, device, asset, location, record, … (12 groups) |
| Zoho Office Integrator *(extended)* | `officeintegrator` | 49 | document, template, fillable, pdf, spreadsheet, presentation, … (7 groups) |
| Zoho PDF Editor *(extended)* | `pdfeditor` | 52 | document, image, page, pagenumber, watermark, link, … (12 groups) |
| Zoho Show *(extended)* | `show` | 65 | presentation, member, publish, workspace, theme, slide, … (15 groups) |
| Zoho Vani *(extended)* | `vani` | 171 | edition, subscription, template, team, team-member, team-request, … (33 groups) |
| Zoho Vertical Studio *(extended)* | `vertical` | 343 | org, user, record, subform, lead, inventory, … (59 groups) |

Not addable, because Zoho publishes no API for them: **Forms**, **Sites**, **Flow** (no management API), **Notebook**, **TeamInbox**, **RouteIQ**, **CommandCenter**, **LandingPage**, **CommunitySpaces**. Reach their data through the products that do have one (Creator, Analytics, CRM).

A few services sit on hosts outside `*.zoho.<dc>`: POS is the former Zakya (`api.zakya.com|in`), Vani is `api.app.vanihq.<dc>`, Office Integrator is `api.office-integrator.<dc>`. All are on zone's host allow-list, so a token or key is never sent anywhere else. **Office Integrator has no OAuth at all** — it authenticates with an API key you store once: `zone ctx officeintegrator apikey=<key>` (and `dc=eu` if your account is not on .com); `zone login officeintegrator` tells you the same.

Beyond REST rows, a few commands carry real logic: `zone crm pull` snapshots an org's metadata (modules, fields, picklists, layouts, Deluge functions) to disk; `zone books pull` and `zone inventory pull` walk every organization's settings into a folder each; `zone crm fn push/pull/test/invoke` and `zone crm fmt` form a Deluge toolchain; `zone transcribe` runs Whisper locally on meeting recordings; `zone zet` wraps Zoho's Marketplace widget toolkit; `zone bug` files a bug report without a GitHub account. Each has `--help` with examples.

## Authentication

- `zone login [services...]` — one browser consent for the named services; `--all` for every core service; `--dc eu` (or `in`, `com.au`, `jp`, `uk`, `ca`, `sa`, `ae`, `sg`, `cn`) for another datacenter; `--no-browser` prints the consent URL for headless machines. Omit `--dc` in a terminal to be prompted.
- Scopes are minimized before the consent URL is built (a broader scope in the same list implies the narrower ones), and a request too long for one URL is split into consent rounds. `--raw-scopes` and `ZONE_CONSENT_BUDGET` adjust that.
- Zoho keeps 20 refresh tokens per client per user and silently invalidates the rest, so grants no service points at are revoked after each login; `zone grants` lists them, `zone grants --prune` cleans up by hand, `--no-prune` opts out.
- `zone status [--live]` shows auth state per service; `--live` probes a real read-only endpoint each. `zone token <service>` prints a valid access token for scripts. `zone logout [services...]` removes sessions.
- The default OAuth client is shared. Bring your own with `--client-id` / `--client-secret` or `ZONE_CLIENT_ID` / `ZONE_CLIENT_SECRET` (redirect URI `http://localhost:14893/zoho/callback`, or your own port with `--port`).
- Session files are mode `0600`, one per service, under the store `zone status` names.
- API-key products (Office Integrator) have no session: `zone status` reports them as `ok (api key)` once `zone ctx <service> apikey=<key>` is stored, and the key rides on every request from context.

## Environment variables

| Variable | Purpose |
|---|---|
| `ZONE_HOME` | Session directory; overrides both the project-local `./.zone` and `~/.zone` |
| `ZONE_CLIENT_ID` / `ZONE_CLIENT_SECRET` | Custom OAuth client |
| `ZONE_PORT` | OAuth callback port (default `14893`) |
| `ZONE_TIMEOUT` | Per-request timeout in ms (default `60000`; `300000` for uploads and downloads) |
| `ZONE_RETRIES` | How many times a throttled (429) or transient call is retried before exit 7 (default `3`; `0` disables) |
| `ZONE_RETRY_MAX_WAIT_MS` | Longest single wait zone will sit out for a retry (default `30000`); a reset further away fails fast with the reset time |
| `ZONE_JSON_ERRORS` | `1` → the JSON error envelope on stderr's last line, without `--json` |
| `ZONE_POLICY` / `ZONE_POLICY_DENY` | Allow / deny rules, e.g. `*:GET` — same grammar as `--allow` / `--deny` |
| `ZONE_AUDIT` | Audit log path — same as `--audit` |
| `ZONE_API_VERSION` | Pin the Zoho API version (e.g. `v8`) — same as `--api-version` |
| `ZONE_API_FALLBACK` | `0` disables the automatic CRM API-version fallback (v9 → v8 → … → v2 when the default version fails with a version-drift signature) |
| `ZONE_CONSENT_BUDGET` | Max consent URL length before a login is split into rounds (default `7500`) |
| `ZONE_BUG_ENDPOINT` | Bug relay for `zone bug`; unset means nothing is ever sent |
| `ZONE_NO_UPDATE_CHECK` | `1` → no "newer version" notice (`NO_UPDATE_NOTIFIER` and `CI=true` also) |
| `NO_COLOR` / `DEBUG` | Disable colours / `true` prints error details |

## Troubleshooting

- **`zone status --live`** is the fastest diagnosis: per service, does the stored grant work right now?
- **Exit `3`** — not signed in for that service: `zone login <service>`. **Exit `4`** — the grant lacks a scope: `zone login <service>` again picks up the current scope set.
- **Exit `5`** — Zoho rejected the call. The `zoho` field carries Zoho's own body; `--verbose` prints full details (redacted). CRM names the offending field in `details.param_name`, and the `hint` translates the common ones.
- **"belongs to multiple organizations"** — set the org once: `zone ctx <service> organization_id=<id>` (discover with `zone api <service> GET /organizations`).
- **Git Bash rewrites `/Leads` into a Windows path.** zone detects and undoes it; if you still see a filesystem path in an error, prefix the command with `MSYS_NO_PATHCONV=1`.
- **Something looks wrong?** `zone bug "<title>" -m "<what happened>"` collects versions, platform, the command shape and the structured error, shows you the exact payload and asks before sending anything. It never includes tokens, `.zone` contents or request/response bodies, and `--dry-run`, `--print` and `--out` let you see or save the report without transmitting it.

## Development

`zone` is a thin CLI over four packages, each published on its own and developed in sibling repos:

| Package | Owns |
|---|---|
| [`@wanasapps/deluge-core`](https://www.npmjs.com/package/@wanasapps/deluge-core) | Deluge formatter, signature parser, grammar, snippets — no I/O |
| [`@wanasapps/zoho-auth`](https://www.npmjs.com/package/@wanasapps/zoho-auth) | OAuth, datacenters, the session store |
| [`@wanasapps/zoho-api`](https://www.npmjs.com/package/@wanasapps/zoho-api) | Service registry and the REST client, including the Zoho host allow-list |
| [`@wanasapps/zcrm-core`](https://www.npmjs.com/package/@wanasapps/zcrm-core) | CRM services and the metadata extractor behind `zone crm pull` |

```bash
npm install && npm test          # offline suite, no sessions needed
npm run relink                   # re-point @wanasapps deps at sibling working copies (any npm install replaces the links)
npm run gen:commands             # regenerate docs/COMMANDS.md from the specs
npm run gen:docs                 # regenerate docs/zone-commands.html
npm run publish:all -- --dry-run # plan a bottom-up publish of every package that is ahead of npm
```

**Where commands come from.** Each service's commands are declarative rows (method, path, args, flags, notes) that `src/core/commandSpec.js` turns into `zone <service> <group> <name>`:

| Location | Holds |
|---|---|
| `src/specs/<service>.js` (or `src/specs/crm/`, `src/specs/desk/`) | hand-written rows, checked against live orgs |
| `src/specs/oas-extras/<service>.js` | rows generated from Zoho's own OpenAPI specs for operations the hand-written file lacks — merged automatically by the loader; every row's `notes` name its source file and operationId, and say whether a live call has confirmed it |
| `src/specs/finance-shared/` | resources Books and Inventory serve identically (purchase returns, item masters and variants, categories), loaded by both |
| `src/core/guides/<service>.md` | the hand-written part of each service's skill: payload shapes and traps |

Coverage is audited against Zoho's published definitions where they exist — `openapi-all.zip` bundles on zoho.com, the `zoho/*-oas` repos on GitHub, and CRM's runtime `GET /crm/<v>/__apis` registry (`scripts/discover-crm-apis.js`). `src/core/specValidate.js` rejects a row whose path placeholder is neither a declared arg nor a known context key.

`prepublishOnly` runs `scripts/check-publish.js` (shebang line endings, shipped files, lockfile versions inside their ranges, every `@wanasapps` range resolving on npm) and then the suite. The history of what changed in each release is in [CHANGELOG.md](CHANGELOG.md).

Library use is possible but secondary:

```js
const zone = require('wanas-zone-cli');
const org = await zone.api.request('crm', 'GET', '/org');
```

## Support

- **Issues:** [github.com/Wanas-Apps/wanas-zone-cli/issues](https://github.com/Wanas-Apps/wanas-zone-cli/issues) — or `zone bug` from the terminal
- **Email:** [support@wanasapps.com](mailto:support@wanasapps.com)
- **Website:** [www.wanasapps.com](https://www.wanasapps.com)

**[Wanas Apps](https://www.wanasapps.com)** is a Zoho Premium Partner and technical consulting firm in Ras al-Khaimah and Cairo, building Zoho implementations, Catalyst applications and integrations for the MENA region. `zone` is the tool we use to do that work.

<p align="center"><sub>Maintained by <strong>Wanas Apps</strong> · ISC License</sub></p>
