@lahat-group/cli 2.1.3 → 2.1.4

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/dist/index.js CHANGED
@@ -86,5 +86,5 @@ To authorize this CLI, visit:
86
86
  `);for(let s of i.warnings)process.stdout.write(`warning: ${s}
87
87
  `)})}u();import{fileURLToPath as Qn}from"url";import{dirname as en,join as N}from"path";import{homedir as Xn}from"os";import{existsSync as Zn,mkdirSync as er,readdirSync as tr,copyFileSync as nr,statSync as rr}from"fs";function ir(){let r=en(Qn(import.meta.url));for(let e=0;e<6;e++){let t=N(r,"skill");if(Zn(N(t,"SKILL.md")))return t;r=en(r)}throw new Error("Bundled SKILL.md not found next to the CLI package")}function or(r){let e=r?N(process.cwd(),".claude"):N(Xn(),".claude");return N(e,"skills","wardrobe-configurator")}function sr(r={}){let e=r.sourceDir??ir(),t=r.targetDir??or(!!r.project);er(t,{recursive:!0});let n=[];for(let i of tr(e)){let o=N(e,i);rr(o).isFile()&&(nr(o,N(t,i)),n.push(i))}return{target:t,files:n}}function tn(r){let e=sr({project:r.project});h(e,!!r.json,()=>{process.stdout.write(`Installed ${e.files.length} file(s) to ${e.target}
88
88
  `),process.stdout.write(`Restart Claude Code, then invoke /wardrobe-configurator
89
- `)})}u();async function ar(r){if(!(r instanceof O))return null;try{return await r.response.json()}catch{return null}}async function nn(r,e){let t=await ar(r);if(e)return JSON.stringify(t?{error:t.error??"request_failed",...t}:{error:r.message},null,2);if(!t)return`Error: ${r.message}`;let n=[t.error??"Request failed"];return t.detail&&n.push(t.detail),t.fix_hint&&n.push(`fix: ${t.fix_hint}`),t.issues&&n.push(JSON.stringify(t.issues)),`Error: ${n.join(" \u2014 ")}`}u();var rn="2.1.3";function ee(r){return r.option("--assembly","buy assembly for this design").option("--no-assembly","do not buy assembly").option("--transport","buy transport for this design").option("--no-transport","do not buy transport").option("--measurement","buy a measurement visit for this design").option("--no-measurement","do not buy a measurement visit")}b.name("wardrobe").description("Wardrobe configurator CLI").version(rn);b.command("login").description("Authenticate via the OAuth device flow").option("--json","machine-readable output").action(r=>jt(r));b.command("whoami").description("Show the authenticated user").option("--json","machine-readable output").action(r=>vt(r));b.command("logout").description("Remove the stored agent key").option("--json","machine-readable output").action(r=>Ot(r));b.command("init").description("Emit a fresh, valid WardrobeConfig JSON (via the API)").option("--width <cm>","total width in cm, as measured on the wall (gross; a niche: its opening)",r=>Number(r)).option("--height <cm>","wardrobe height in cm (the ceiling follows, 3 cm above)",r=>Number(r)).option("--ceiling <cm>","room ceiling height in cm (the wardrobe stands 3 cm under it)",r=>Number(r)).option("--depth <cm>","depth in cm, doors included (default 60)",r=>Number(r)).option("--position <pos>","right-corner (default) | left-corner | niche | freestanding").action(r=>yt(r));b.command("validate <file>").description("Validate a config against the live rules (file or - for stdin)").option("--json","machine-readable output").action((r,e)=>wt(r,e));b.command("describe <idOrFile>").description("Describe a config (local file/-) or saved design (id), via the API").option("--locale <locale>","en or he","en").option("--json","machine-readable output").action((r,e)=>_t(r,e));b.command("catalog <kind>").description(`List what a config may name, from the live catalogs: ${ke.join(" | ")}. Read this before choosing a colour, handle or aluminium front \u2014 never invent ids`).option("--json","machine-readable output").action((r,e)=>Yt(r,e));b.command("prompt <projectId> <text>").description("Describe a wardrobe in words (he/en) and get a draft config file + the assumptions made. Saves nothing; counts against a daily per-key limit; takes 10-30 s").option("--locale <locale>","he or en","he").option("--out <file>","where to write the draft config (default ./prompt-<timestamp>.json)").option("--json","print the whole response (config, assumptions, adjustments, warnings) instead of writing a file").action((r,e,t)=>Zt(r,e,t));b.command("price <file>").description("Price a config via the API (file or - for stdin)").option("--json","machine-readable output").action((r,e)=>Vt(r,e));b.command("plan <file>").description("Dry-run: price a config + BOM preview (mutates nothing)").option("--json","machine-readable output").action((r,e)=>Bt(r,e));ee(b.command("apply <file>").description("Save a config as a design (file or - for stdin)").requiredOption("--project <id>","project id").option("--name <name>","design name").option("--json","machine-readable output")).action((r,e)=>Pe(r,e));var I=b.command("designs").description("Manage saved designs");I.command("list").description("List designs in a project, or the newest across every project with --project omitted").option("--project <id>","project id").option("--limit <n>","cross-project mode only: max rows (default 20, max 100)",r=>Number(r)).option("--since <iso>","cross-project mode only: only designs updated at/after this ISO 8601 timestamp").option("--json","machine-readable output").action(r=>Ft(r));ee(I.command("create <file>").description("Save a config as a new design in a project (same as `apply`)").requiredOption("--project <id>","project id").option("--name <name>","design name").option("--json","machine-readable output")).action((r,e)=>Pe(r,e));I.command("get <id>").description("Show one saved design with its stored config \u2014 read this before `designs update`, which replaces the whole config").option("--config","print only the bare config: exactly what `designs update --file` accepts").option("--json","machine-readable output: id, projectId, name, version, nisTotal, serviceTotal, config").action((r,e)=>Tt(r,e));ee(I.command("update <id>").description("Revise a saved design (config, name and/or services). Services you do not mention keep their current value").option("--file <file>","new config JSON (file or - for stdin)").option("--name <name>","new design name").option("--json","machine-readable output")).action((r,e)=>Ut(r,e));ee(I.command("duplicate <id>").description("Copy a design (into the same project unless --project is given). Services are not copied \u2014 pass the flags to set them").option("--project <id>","target project (default: the source design's project)").option("--name <name>",'name of the copy (default: "<name> \u05E2\u05D5\u05EA\u05E7")').option("--json","machine-readable output")).action((r,e)=>zt(r,e));I.command("delete <id>").description("Archive a design (soft delete; there is no restore in the app). Requires --yes").option("--yes","confirm the archive").option("--json","machine-readable output").action((r,e)=>Jt(r,e));I.command("thumbnail <id>").description("Download a design's thumbnail image; renders it on the server first if none exists yet").option("--out <file>","destination path").option("--render","re-render on the server even if a thumbnail exists").option("--json","machine-readable output").action((r,e)=>Nt(r,e));var Re=b.command("projects").description("Manage projects");Re.command("list").description("List accessible projects").option("--phone <number>","filter by the customer's phone number").option("--designer <idOrName>","filter by the designer who owns the project").option("--json","machine-readable output").action(r=>Mt(r));Re.command("get <id>").description("Show one project").option("--json","machine-readable output").action((r,e)=>$t(r,e));Re.command("members <id>").description("List a project's owner and members").option("--json","machine-readable output").action((r,e)=>Ct(r,e));var Te=b.command("customers").description("Read customers");Te.command("list").description("List your customers").option("--json","machine-readable output").action(r=>At(r));Te.command("get <id>").description("Show one customer").option("--json","machine-readable output").action((r,e)=>St(r,e));Te.command("projects <id>").description("List a customer's projects").option("--json","machine-readable output").action((r,e)=>Et(r,e));var te=b.command("orders").description("Read orders, or move one along the workflow (orders:transition, office accounts only)");te.command("list").description("List a project's orders, or the newest-updated across every project with --project omitted").option("--project <id>","project id").option("--open","only the project's active (non-cancelled) order (requires --project)").option("--limit <n>","cross-project mode only: max rows (default 20, max 100)",r=>Number(r)).option("--since <iso>","cross-project mode only: only orders updated at/after this ISO 8601 timestamp").option("--json","machine-readable output").action(r=>Pt(r));te.command("get <id>").description("Show one order, with its designs embedded").option("--json","machine-readable output").action((r,e)=>kt(r,e));te.command("pdf <id>").description("Download an order PDF").option("--out <file>","destination path").option("--json","machine-readable output").action((r,e)=>Rt(r,e));te.command("set-status <id> <status>").description("Move an order to its next status (e.g. paid, pending_assembly); only legal steps, never cancelled").option("--dry-run","check the step is allowed without changing anything").option("--note <text>","kept on the order's status history (e.g. a receipt number)").option("--json","machine-readable output").action((r,e,t)=>Gt(r,e,t));var qe=b.command("quotations").description("Read quotations (quotations:read), or mint a revision (quotations:write)");qe.command("list").description("Show a project's quote history, or the newest-generated across every project with --project omitted").option("--project <id>","project id").option("--limit <n>","cross-project mode only: max rows (default 20, max 100)",r=>Number(r)).option("--since <iso>","cross-project mode only: only quotations generated at/after this ISO 8601 timestamp").option("--json","machine-readable output").action(r=>Lt(r));qe.command("pdf <projectId>").description("Download a quotation PDF (default: the project's current revision) \u2014 never mints a new one").option("--revision <n>","a specific historical revision instead of the current one",r=>Number(r)).option("--out <file>","destination path").option("--json","machine-readable output").action((r,e)=>qt(r,e));qe.command("mint <projectId>").description("File the project's quotation at its current price and download the PDF \u2014 unlike `pdf`, this can mint a new revision (requires quotations:write)").option("--out <file>","destination path").option("--json","machine-readable output: path, bytes, quoteNumber, revision, minted").action((r,e)=>Wt(r,e));var on=b.command("users").description("Read registered accounts (requires staff:read)");on.command("list").description("List registered users, newest-registered first").option("--limit <n>","max rows (default 20, max 100)",r=>Number(r)).option("--since <iso>","only users registered at/after this ISO 8601 timestamp").option("--before <iso>","only users registered before this ISO 8601 timestamp (page back with the last row's createdAt)").option("--role <role>","only users with this role: admin, support, designer, client").option("--json","machine-readable output").action(r=>It(r));on.command("by-phone <phone>").description("Find the account(s) whose own phone is this number (+972\u2026 and 05\u2026 forms match)").option("--json","machine-readable output").action((r,e)=>Dt(r,e));b.command("help-texts").description("Read the help-text table").option("--json","machine-readable output").action(r=>Ht(r));var cr=b.command("skill").description("Manage the bundled Claude Skill");cr.command("install").description("Install the wardrobe-configurator Skill for Claude Code").option("--project","install into ./.claude/skills instead of ~/.claude/skills").option("--json","machine-readable output").action(r=>tn(r));b.parseAsync().catch(async r=>{let e=await nn(r,process.argv.includes("--json"));process.stderr.write(`${e}
89
+ `)})}u();async function ar(r){if(!(r instanceof O))return null;try{return await r.response.json()}catch{return null}}async function nn(r,e){let t=await ar(r);if(e)return JSON.stringify(t?{error:t.error??"request_failed",...t}:{error:r.message},null,2);if(!t)return`Error: ${r.message}`;let n=[t.error??"Request failed"];return t.detail&&n.push(t.detail),t.fix_hint&&n.push(`fix: ${t.fix_hint}`),t.issues&&n.push(JSON.stringify(t.issues)),`Error: ${n.join(" \u2014 ")}`}u();var rn="2.1.4";function ee(r){return r.option("--assembly","buy assembly for this design").option("--no-assembly","do not buy assembly").option("--transport","buy transport for this design").option("--no-transport","do not buy transport").option("--measurement","buy a measurement visit for this design").option("--no-measurement","do not buy a measurement visit")}b.name("wardrobe").description("Wardrobe configurator CLI").version(rn);b.command("login").description("Authenticate via the OAuth device flow").option("--json","machine-readable output").action(r=>jt(r));b.command("whoami").description("Show the authenticated user").option("--json","machine-readable output").action(r=>vt(r));b.command("logout").description("Remove the stored agent key").option("--json","machine-readable output").action(r=>Ot(r));b.command("init").description("Emit a fresh, valid WardrobeConfig JSON (via the API)").option("--width <cm>","total width in cm, as measured on the wall (gross; a niche: its opening)",r=>Number(r)).option("--height <cm>","wardrobe height in cm (the ceiling follows, 3 cm above)",r=>Number(r)).option("--ceiling <cm>","room ceiling height in cm (the wardrobe stands 3 cm under it)",r=>Number(r)).option("--depth <cm>","depth in cm, doors included (default 60)",r=>Number(r)).option("--position <pos>","right-corner (default) | left-corner | niche | freestanding").action(r=>yt(r));b.command("validate <file>").description("Validate a config against the live rules (file or - for stdin)").option("--json","machine-readable output").action((r,e)=>wt(r,e));b.command("describe <idOrFile>").description("Describe a config (local file/-) or saved design (id), via the API").option("--locale <locale>","en or he","en").option("--json","machine-readable output").action((r,e)=>_t(r,e));b.command("catalog <kind>").description(`List what a config may name, from the live catalogs: ${ke.join(" | ")}. Read this before choosing a colour, handle or aluminium front \u2014 never invent ids`).option("--json","machine-readable output").action((r,e)=>Yt(r,e));b.command("prompt <projectId> <text>").description("Describe a wardrobe in words (he/en) and get a draft config file + the assumptions made. Saves nothing; counts against a daily per-key limit; takes 10-30 s").option("--locale <locale>","he or en","he").option("--out <file>","where to write the draft config (default ./prompt-<timestamp>.json)").option("--json","print the whole response (config, assumptions, adjustments, warnings) instead of writing a file").action((r,e,t)=>Zt(r,e,t));b.command("price <file>").description("Price a config via the API (file or - for stdin)").option("--json","machine-readable output").action((r,e)=>Vt(r,e));b.command("plan <file>").description("Dry-run: price a config + BOM preview (mutates nothing)").option("--json","machine-readable output").action((r,e)=>Bt(r,e));ee(b.command("apply <file>").description("Save a config as a design (file or - for stdin)").requiredOption("--project <id>","project id").option("--name <name>","design name").option("--json","machine-readable output")).action((r,e)=>Pe(r,e));var I=b.command("designs").description("Manage saved designs");I.command("list").description("List designs in a project, or the newest across every project with --project omitted").option("--project <id>","project id").option("--limit <n>","cross-project mode only: max rows (default 20, max 100)",r=>Number(r)).option("--since <iso>","cross-project mode only: only designs updated at/after this ISO 8601 timestamp").option("--json","machine-readable output").action(r=>Ft(r));ee(I.command("create <file>").description("Save a config as a new design in a project (same as `apply`)").requiredOption("--project <id>","project id").option("--name <name>","design name").option("--json","machine-readable output")).action((r,e)=>Pe(r,e));I.command("get <id>").description("Show one saved design with its stored config \u2014 read this before `designs update`, which replaces the whole config").option("--config","print only the bare config: exactly what `designs update --file` accepts").option("--json","machine-readable output: id, projectId, name, version, nisTotal, serviceTotal, config").action((r,e)=>Tt(r,e));ee(I.command("update <id>").description("Revise a saved design (config, name and/or services). Services you do not mention keep their current value").option("--file <file>","new config JSON (file or - for stdin)").option("--name <name>","new design name").option("--json","machine-readable output")).action((r,e)=>Ut(r,e));ee(I.command("duplicate <id>").description("Copy a design (into the same project unless --project is given). Services are not copied \u2014 pass the flags to set them").option("--project <id>","target project (default: the source design's project)").option("--name <name>",'name of the copy (default: "<name> \u05E2\u05D5\u05EA\u05E7")').option("--json","machine-readable output")).action((r,e)=>zt(r,e));I.command("delete <id>").description("Archive a design (soft delete; there is no restore in the app). Requires --yes").option("--yes","confirm the archive").option("--json","machine-readable output").action((r,e)=>Jt(r,e));I.command("thumbnail <id>").description("Download a design's thumbnail image; renders it on the server first if none exists yet").option("--out <file>","destination path").option("--render","re-render on the server even if a thumbnail exists").option("--json","machine-readable output").action((r,e)=>Nt(r,e));var Re=b.command("projects").description("Manage projects");Re.command("list").description("List accessible projects").option("--phone <number>","filter by the customer's phone number").option("--designer <idOrName>","filter by the designer who owns the project").option("--json","machine-readable output").action(r=>Mt(r));Re.command("get <id>").description("Show one project").option("--json","machine-readable output").action((r,e)=>$t(r,e));Re.command("members <id>").description("List a project's owner and members").option("--json","machine-readable output").action((r,e)=>Ct(r,e));var Te=b.command("customers").description("Read customers");Te.command("list").description("List your customers").option("--json","machine-readable output").action(r=>At(r));Te.command("get <id>").description("Show one customer").option("--json","machine-readable output").action((r,e)=>St(r,e));Te.command("projects <id>").description("List a customer's projects").option("--json","machine-readable output").action((r,e)=>Et(r,e));var te=b.command("orders").description("Read orders, or move one along the workflow (orders:transition, office accounts only)");te.command("list").description("List a project's orders, or the newest-updated across every project with --project omitted").option("--project <id>","project id").option("--open","only the project's active (non-cancelled) order (requires --project)").option("--limit <n>","cross-project mode only: max rows (default 20, max 100)",r=>Number(r)).option("--since <iso>","cross-project mode only: only orders updated at/after this ISO 8601 timestamp").option("--json","machine-readable output").action(r=>Pt(r));te.command("get <id>").description("Show one order, with its designs embedded").option("--json","machine-readable output").action((r,e)=>kt(r,e));te.command("pdf <id>").description("Download an order PDF").option("--out <file>","destination path").option("--json","machine-readable output").action((r,e)=>Rt(r,e));te.command("set-status <id> <status>").description("Move an order to its next status (e.g. paid, pending_assembly); only legal steps, never cancelled").option("--dry-run","check the step is allowed without changing anything").option("--note <text>","kept on the order's status history (e.g. a receipt number)").option("--json","machine-readable output").action((r,e,t)=>Gt(r,e,t));var qe=b.command("quotations").description("Read quotations (quotations:read), or mint a revision (quotations:write)");qe.command("list").description("Show a project's quote history, or the newest-generated across every project with --project omitted").option("--project <id>","project id").option("--limit <n>","cross-project mode only: max rows (default 20, max 100)",r=>Number(r)).option("--since <iso>","cross-project mode only: only quotations generated at/after this ISO 8601 timestamp").option("--json","machine-readable output").action(r=>Lt(r));qe.command("pdf <projectId>").description("Download a quotation PDF (default: the project's current revision) \u2014 never mints a new one").option("--revision <n>","a specific historical revision instead of the current one",r=>Number(r)).option("--out <file>","destination path").option("--json","machine-readable output").action((r,e)=>qt(r,e));qe.command("mint <projectId>").description("File the project's quotation at its current price and download the PDF \u2014 unlike `pdf`, this can mint a new revision (requires quotations:write)").option("--out <file>","destination path").option("--json","machine-readable output: path, bytes, quoteNumber, revision, minted").action((r,e)=>Wt(r,e));var on=b.command("users").description("Read registered accounts (requires staff:read)");on.command("list").description("List registered users, newest-registered first").option("--limit <n>","max rows (default 20, max 100)",r=>Number(r)).option("--since <iso>","only users registered at/after this ISO 8601 timestamp").option("--before <iso>","only users registered before this ISO 8601 timestamp (page back with the last row's createdAt)").option("--role <role>","only users with this role: admin, support, designer, client").option("--json","machine-readable output").action(r=>It(r));on.command("by-phone <phone>").description("Find the account(s) whose own phone is this number (+972\u2026 and 05\u2026 forms match)").option("--json","machine-readable output").action((r,e)=>Dt(r,e));b.command("help-texts").description("Read the help-text table").option("--json","machine-readable output").action(r=>Ht(r));var cr=b.command("skill").description("Manage the bundled Claude Skill");cr.command("install").description("Install the wardrobe-configurator Skill for Claude Code").option("--project","install into ./.claude/skills instead of ~/.claude/skills").option("--json","machine-readable output").action(r=>tn(r));b.parseAsync().catch(async r=>{let e=await nn(r,process.argv.includes("--json"));process.stderr.write(`${e}
90
90
  `),process.exit(1)});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lahat-group/cli",
3
- "version": "2.1.3",
3
+ "version": "2.1.4",
4
4
  "description": "CLI + Claude Skill to build, validate, price, and save wardrobe designs.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
package/skill/SKILL.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: wardrobe-configurator
3
- description: Use when the user wants to build, configure, validate, price, or order a wardrobe/closet design from the terminal — phrases like "configure a wardrobe", "price a design", "build a closet layout", "validate my wardrobe config" — OR wants to look up existing wardrobe data: projects, customers, designers, orders, quotations and their PDFs, or (staff only) registered users across the whole system, e.g. "which projects does this designer have", "find the customer with phone 05x", "who is on this project", "what is the status of this order", "download the order PDF", "who signed up recently", "which account owns this phone number", "what's the latest quote we generated". Drives the `wardrobe` CLI (HTTP client over the configurator API); a WardrobeConfig JSON file on disk is the unit of work for design verbs, and every lookup verb is read-only.
3
+ description: Use when a designer (or office staff) wants to design, price, revise or quote a Lahat Pro wardrobe/closet from the terminal — phrases like "design a wardrobe for my customer", "a 2.4 m wardrobe with drawers", "price this design", "make it 3 m wide", "send the customer a quote", "validate my wardrobe config" — OR to look up their own projects, customers, designs, orders and quotations and their PDFs, e.g. "find the customer with phone 05x", "who is on this project", "what is the status of this order", "download the order PDF". Office staff keys can also look up registered users and other designers' projects (staff.md). Drives the `wardrobe` CLI (HTTP client over the configurator API); a WardrobeConfig JSON file on disk is the unit of work for design verbs, and every lookup verb is read-only.
4
4
  ---
5
5
 
6
6
  # Wardrobe Configurator CLI
@@ -9,6 +9,11 @@ Build, validate, price, save, and stage wardrobe designs from the terminal via t
9
9
  `wardrobe` CLI. A **`WardrobeConfig` JSON file on disk is the unit of work**. The
10
10
  agent loop is: **init → edit → validate → price → plan → apply**.
11
11
 
12
+ **Start with `wardrobe whoami --json`.** It tells you who you work for (`role`) and what
13
+ the key may do (`agent.scopes`). This file is everything a designer needs. If the scopes
14
+ include `staff:read` or `orders:transition`, the key belongs to office staff — also read
15
+ [staff.md](staff.md) for the extra verbs; otherwise don't reach for them.
16
+
12
17
  ## Never invent an id — read the catalog
13
18
 
14
19
  A config names colours, a handle and (optionally) aluminium door fronts by **id**, and
@@ -26,6 +31,11 @@ quietly substituting a default colour. So before choosing one, read the live lis
26
31
  Old ids you may remember (`white-melamine`, `oak-veneer`, `interior-eco-white`) are
27
32
  retired — they are not in `catalog materials`, so don't use them.
28
33
 
34
+ **Ask for the exterior colour and the handle** before you save a design for a customer.
35
+ They are the two visible choices with no business default: left out, the design gets the
36
+ catalog's placeholder colour (turquoise) and whichever handle the catalog marks
37
+ `(default)`, which is rarely what anyone wanted.
38
+
29
39
  ## Prerequisites
30
40
 
31
41
  - Install the CLI: `npm install -g @lahat-group/cli` (or run ad-hoc via `npx @lahat-group/cli <verb>`).
@@ -42,6 +52,21 @@ retired — they are not in `catalog materials`, so don't use them.
42
52
  (e.g. `http://localhost:3001/api` for a local dev server).
43
53
  - Every verb accepts `--json` for machine-readable output. Read those; don't scrape prose.
44
54
 
55
+ ## Where a design goes — customers, projects and your Play room
56
+
57
+ Every design is saved into a **project**, and every project belongs to a **customer**.
58
+ `wardrobe projects list` shows yours with each one's customer; `--phone <number>` finds a
59
+ customer's project by their number.
60
+
61
+ - **The CLI cannot create a customer or a project.** For a new customer, ask the user to
62
+ add the customer in the web app and open them (that gives the customer their project),
63
+ then `wardrobe projects list` to get its id.
64
+ - **Your Play room** is the project whose customer is you (your own name on both). It is a
65
+ sandbox for trying things out — save experiments there, never a real customer's design,
66
+ and archive what you no longer need.
67
+ - A customer invited into a project is a **member** (`wardrobe projects members <id>`) and
68
+ sees every design saved there. Don't save drafts or experiments into a customer's project.
69
+
45
70
  ## The loop
46
71
 
47
72
  1. **init** — `wardrobe init [--width <cm>] [--position <pos>] [--height <cm> | --ceiling <cm>] [--depth <cm>] > wardrobe.json`
@@ -54,7 +79,7 @@ retired — they are not in `catalog materials`, so don't use them.
54
79
  so a config `validate` approves can never be one `price` rejects.
55
80
  Exit 0 = valid; exit 1 + structured errors otherwise. Do this after every edit.
56
81
  4. **price** — `wardrobe price wardrobe.json` → authoritative total in **NIS** from
57
- the API's unified area×rate engine.
82
+ the API's unified area×rate engine — **before VAT** (see below).
58
83
  5. **plan** — `wardrobe plan wardrobe.json` → dry-run price **plus a BOM preview**.
59
84
  Mutates nothing. Use this to preview before applying.
60
85
  6. **apply** — `wardrobe apply wardrobe.json --project <id> [--name <name>]` → saves
@@ -62,6 +87,17 @@ retired — they are not in `catalog materials`, so don't use them.
62
87
  required.** `wardrobe designs create <file> --project <id>` is the same verb under
63
88
  the `designs` group, if that reads more clearly in context.
64
89
 
90
+ ### Prices are before VAT
91
+
92
+ Every number the CLI prints — `price`, `plan`, `designs get` (`nisTotal`, `serviceTotal`)
93
+ — is **before VAT**. The web app, the quotation PDF and the customer all see it **with
94
+ VAT, rounded to whole shekels**: `round(price × (1 + vatRate))`, where `vatRate` comes
95
+ from `wardrobe catalog service-pricing` (0.18 today). A design that prices at 5,052.94
96
+ here shows as **5,962** on the website. When you tell the user a price, say which one it
97
+ is — and use the VAT-inclusive figure for anything the customer will see. Services
98
+ (`serviceTotal`) are separate and also before VAT; the project total the customer pays is
99
+ the designs plus the services they bought, with VAT.
100
+
65
101
  ### Lahat Pro defaults — apply them unless the user says otherwise
66
102
 
67
103
  `init`, `wardrobe prompt` and the web wizard all start from these; keep to them when you
@@ -92,7 +128,7 @@ If the user describes what they want instead of giving dimensions —
92
128
  "a 2.4 m wardrobe, drawers in the middle, glass doors on the first column" — use
93
129
  `wardrobe prompt <projectId> "<their words>" [--locale he|en] [--out draft.json]`. It
94
130
  writes a draft config to a file (default `./prompt-<timestamp>.json`) and prints the
95
- **assumptions** it made (e.g. "assumed freestanding"), plus any adjustments and warnings.
131
+ **assumptions** it made (e.g. "assumed a right corner"), plus any adjustments and warnings.
96
132
  Saves nothing. Then: **show the user the assumptions and ask them to confirm** → `validate`
97
133
  → `price` → `apply`. Treat the draft as a starting point, never as a finished design.
98
134
 
@@ -143,8 +179,8 @@ Two refusals to expect, neither of which is retryable:
143
179
  user's intent on top, and tell them it changed underneath you. **Never** loop.
144
180
  - **`423 projectLocked`** — the project has a live (non-cancelled) order, which froze
145
181
  its designs. No design in it can be created, updated or deleted, and no new quotation
146
- revision minted, until a human cancels that order — **even with an admin's key**, which the
147
- API holds to the same rule. Say so; don't try another design or another verb. (Check
182
+ revision minted, until a human cancels that order — whoever's key it is. Say so; don't
183
+ try another design or another verb. (Check
148
184
  `wardrobe orders list --project <id> --open` first to tell the user *why* before trying.)
149
185
 
150
186
  Note that updating a config **clears the design's thumbnail** (the stored image was of
@@ -165,24 +201,22 @@ or a saved design id.
165
201
 
166
202
  ## Reading what exists
167
203
 
168
- Every read the key can reach has a verb. **All of them are GETs** — the CLI can read
169
- customers, orders and quotations but never create or change them — the one exception is
170
- `orders set-status` for office accounts (`customers:write`,
171
- `orders:write` and `quotations:write` are not on the CLI's key, so those routes
172
- answer `403 insufficient_scope` whoever signed in).
204
+ Every read the key can reach has a verb, and they are all reads — the CLI can read
205
+ customers, projects and orders but never create or change them (only office staff can
206
+ move an order along; see staff.md). Quotations are the one other thing you can write:
207
+ `quotations mint`.
173
208
 
174
209
  | Verb | Shows |
175
210
  |---|---|
176
211
  | `wardrobe projects list` | Projects you can access (needed for `--project`) |
177
212
  | `wardrobe projects list --phone <number>` | Only projects whose **customer** has that number |
178
- | `wardrobe projects list --designer <idOrName>` | Only projects owned by that designer |
179
213
  | `wardrobe projects get <id>` | One project: customer + owning designer |
180
214
  | `wardrobe projects members <id>` | Owner plus every invited member, with roles |
181
215
  | `wardrobe customers list` | Your customers |
182
216
  | `wardrobe customers get <id>` | One customer |
183
217
  | `wardrobe customers projects <id>` | That customer's projects |
184
218
  | `wardrobe designs list --project <id>` | Designs saved in a project |
185
- | `wardrobe designs list` | The newest designs across every project you can see (all yours, or the whole tenant with `staff:read`) |
219
+ | `wardrobe designs list` | The newest designs across every project you can see |
186
220
  | `wardrobe designs list --limit <n>` `--since <ISO>` | Same, paged — `--limit` caps rows (default 20, max 100), `--since` filters by `updatedAt >=` |
187
221
  | `wardrobe designs get <id>` | One design: name, size, live price, version. `--json` adds `projectId`, `nisTotal`, `serviceTotal` and the full `config`; `--config` prints only the bare config (what `designs update --file` reads) |
188
222
  | `wardrobe designs thumbnail <id> --out f.jpg` | The design's rendered thumbnail. A design nobody has opened in a browser has none yet, so the server renders it first (~20 s); `--render` forces a fresh render. `503` means the renderer is down — retry in a minute |
@@ -192,23 +226,19 @@ answer `403 insufficient_scope` whoever signed in).
192
226
  | `wardrobe orders list --limit <n>` `--since <ISO>` | Same, paged (`--since` filters by `updatedAt >=`) |
193
227
  | `wardrobe orders get <id>` | One order, with its designs' frozen config embedded — no second call to `designs` needed. Prints the buyer's delivery choice (`delivery: immediate` or `delivery: scheduled <date>`) once they have verified their details |
194
228
  | `wardrobe orders pdf <id> --out f.pdf` | The order PDF |
195
- | `wardrobe orders set-status <id> <status> [--dry-run] [--note <text>]` | **Office accounts only** (`orders:transition`, admin/support approvers). Moves the order one legal step: `paid`, `pending_measurement`, `sent_to_manufacturing`, `pending_assembly`, `provided` — never `cancelled`. Already there → "nothing changed"; an illegal step answers 409 with the reason (e.g. `pending_assembly` is only reachable from `in_manufacturing` when transport & installation was bought) — report that to a human, don't retry. `--dry-run` checks without changing; `--note` puts a receipt number on the order's history |
196
229
  | `wardrobe quotations list --project <id>` | That project's quote number + full revision history |
197
230
  | `wardrobe quotations list` | Every quotation you can see, **newest-generated first** — this is "latest quotes generation" |
198
231
  | `wardrobe quotations list --limit <n>` `--since <ISO>` | Same, paged (`--since` filters by `updatedAt >=`, i.e. last generated at/after) |
199
232
  | `wardrobe quotations pdf <projectId> --out f.pdf` | The project's **current** quotation revision's PDF — fetches only, never mints one |
200
233
  | `wardrobe quotations pdf <projectId> --revision <n> --out f.pdf` | A specific historical revision's PDF instead |
201
234
  | `wardrobe quotations mint <projectId> --out f.pdf` | File the quotation at the project's **current** price and download it. **Unlike `pdf`, this can mint a new revision** (1, 2, 3…, history kept) when the price moved since the last one; at an unchanged price it re-serves the stored revision. Output says which (`--json`: `minted`). Needs `quotations:write`; refused with `423` once the project has an order. Use it after a `designs update`, never speculatively — a new revision changes the number on the quote the customer holds |
202
- | `wardrobe users list` | Registered accounts, **newest-registered first** — staff only, see below |
203
- | `wardrobe users list --limit <n>` `--since <ISO>` | Same, paged (`--since` filters by `createdAt >=`) — `--limit 5` with no `--since` is "the latest registered users"; omit both, paging with `--since` off the last row's timestamp, for "every user in the system" |
204
- | `wardrobe users by-phone <number>` | The account(s) whose **own** phone is that number (`+972…`, `05…`, `052-…` all match) — staff only |
205
235
  | `wardrobe help-texts` | The help-text table |
206
236
 
207
- `projects list` already returns each project's **customer and owning designer inline** —
208
- so "who is the customer on each of this designer's projects" is one call plus a filter,
209
- never one call per project. Prefer `--phone` / `--designer` over fetching and grepping.
237
+ `projects list` already returns each project's **customer inline** — so "who is the
238
+ customer on each of my projects" is one call, never one call per project. Prefer
239
+ `--phone` over fetching and grepping.
210
240
 
211
- Every cross-project/cross-tenant list (`designs`, `orders`, `quotations`, `users`) takes
241
+ Every cross-project list (`designs`, `orders`, `quotations`) takes
212
242
  `--limit` (1–100, default 20) and a `--since` cursor — pass the last row's timestamp
213
243
  back as `--since` to page further instead of guessing an offset. A malformed `--since` or
214
244
  an out-of-range `--limit` is a `400`, not a silently-ignored filter — read the error
@@ -219,37 +249,14 @@ rather than retrying blind.
219
249
  The list is bounded by the key, so an empty result is as often a **visibility** answer as
220
250
  a data one. `wardrobe whoami --json` reports the truth under `agent.scopes` and `role`:
221
251
 
222
- - An ordinary designer or client sees **their own** projects, designs, orders and
223
- quotations, plus ones they were invited to. A project belonging to someone else
224
- answers `403 Forbidden`, not `404`. `wardrobe users list` and `users by-phone` answer
225
- `403 Forbidden` outright — there is no per-row visibility to fall back to; the whole
226
- surface is staff-only (see below).
227
- - A key approved by an **admin or support** user *and* carrying **`staff:read`** sees
228
- **every** project, customer, order and quotation in the system — and can use
229
- `wardrobe users list` / `users by-phone` at all. That scope is granted only to
230
- staff, only for GETs, and is listed by name on the consent screen. The same approver
231
- also receives **`staff:write`**, which lets `designs create` / `designs update` land in
232
- a project the admin does not own. A key logged in before 2026-09-05 lacks `staff:read`
233
- and before 2026-09-12 lacks `quotations:read` — a `403 insufficient_scope` on
234
- `quotations list`, or a `403 Forbidden` on a write to someone else's project, means
235
- `wardrobe login` again.
252
+ - A designer sees **their own** projects, customers, designs, orders and quotations,
253
+ plus projects they were invited to. A project belonging to someone else answers
254
+ `403 Forbidden`, not `404`.
255
+ - `403 insufficient_scope` means the key lacks that permission — either the verb is for
256
+ office staff (staff.md), or the key is older than the feature: `wardrobe login` again.
236
257
 
237
258
  Never tell a user something does not exist when you only know your key cannot see it.
238
- Say which it is: `403 Forbidden` means the row is someone else's (or, for `users`, that
239
- your key isn't staff at all), `404` means it is gone.
240
-
241
- ### `wardrobe users` is a directory lookup, not a customer lookup
242
-
243
- `users list` / `users by-phone` read the `User` **account** table — designers, admins,
244
- support and self-serve clients who can sign in — not the per-project `Customer` contact
245
- card `wardrobe customers` reads. Each row carries the account's own `phone` (empty until
246
- that user passes the onboarding phone step; staff never do). `users by-phone` matches
247
- **only** that field: a customer's number never resolves to the designer who serves them,
248
- so its answer is "who is this person", safe to act on as the caller's identity. Numbers
249
- match as the national number, so `+972526665153` and `0526665153` are the same. More than
250
- one row means the number is ambiguous (the field is self-declared, not unique) — say so
251
- rather than picking one. Zero rows is normal and not an error; for the customer side of
252
- the same number use `wardrobe projects list --phone <number>`.
259
+ Say which it is: `403 Forbidden` means the row is someone else's, `404` means it is gone.
253
260
 
254
261
  ### Not available here
255
262
 
@@ -284,12 +291,10 @@ Prefer generating the skeleton with `wardrobe init` rather than hand-writing it
284
291
 
285
292
  - **The CLI can read orders but never place one.** Its key carries `profile:read`,
286
293
  `projects:read`, `customers:read`, `designs:read`, `designs:write`, `orders:read`,
287
- `price:read`, `quotations:read`, `quotations:write` (+ `staff:read` and `staff:write` for
288
- staff) — **every write beyond designs and quotations is absent**, and so is every admin
289
- scope. `wardrobe orders list`, `orders pdf`, `quotations list`, `quotations pdf` and
290
- `quotations mint` work; creating or changing an order or a customer answers
291
- `403 insufficient_scope` (a key logged in before 2026-10-02 lacks `quotations:write` —
292
- `wardrobe login` again). A
294
+ `price:read`, `quotations:read`, `quotations:write` — **every write beyond designs and
295
+ quotations is absent**, and so is every admin scope. `wardrobe orders list`,
296
+ `orders pdf`, `quotations list`, `quotations pdf` and `quotations mint` work; creating or
297
+ changing an order or a customer answers `403 insufficient_scope`. A
293
298
  project that has never been quoted on the web app has no revision to fetch —
294
299
  `quotations pdf`/`quotations list --project` answer `404 quotation_not_found`, not an
295
300
  empty document — `quotations mint` is how a first revision is filed. Don't attempt
package/skill/staff.md ADDED
@@ -0,0 +1,59 @@
1
+ # Wardrobe CLI — staff keys (admin / support)
2
+
3
+ Read this only when `wardrobe whoami --json` shows `staff:read` (or `orders:transition`)
4
+ under `agent.scopes`. A designer's or client's key has none of these, and every verb
5
+ below answers `403` for it — don't try them.
6
+
7
+ ## What a staff key sees and may change
8
+
9
+ - A key approved by an **admin or support** user *and* carrying **`staff:read`** sees
10
+ **every** project, customer, order and quotation in the system — and can use
11
+ `wardrobe users list` / `users by-phone` at all. That scope is granted only to
12
+ staff, only for GETs, and is listed by name on the consent screen.
13
+ - The same approver also receives **`staff:write`**, which lets `designs create` /
14
+ `designs update` land in a project the admin does not own.
15
+ - **No key bypasses a locked project.** A project with a live order answers
16
+ `423 projectLocked` even to an admin's key with `staff:write` — the API holds agents
17
+ to the rule the web app relaxes for office staff. Revising an ordered design goes
18
+ through the web app.
19
+ - Old keys lack newer scopes: a key logged in before 2026-09-05 lacks `staff:read`,
20
+ before 2026-09-12 `quotations:read`, before 2026-10-02 `quotations:write`. A
21
+ `403 insufficient_scope` on those, or a `403 Forbidden` on a write to someone else's
22
+ project, means `wardrobe login` again.
23
+
24
+ ## Staff-only verbs
25
+
26
+ | Verb | Shows / does |
27
+ |---|---|
28
+ | `wardrobe projects list --designer <idOrName>` | Only projects owned by that designer |
29
+ | `wardrobe designs list` / `orders list` / `quotations list` | With `staff:read`, the newest across the **whole tenant**, not just yours |
30
+ | `wardrobe users list` | Registered accounts, **newest-registered first** |
31
+ | `wardrobe users list --limit <n>` `--since <ISO>` | Same, paged (`--since` filters by `createdAt >=`) — `--limit 5` with no `--since` is "the latest registered users"; omit both, paging with `--since` off the last row's timestamp, for "every user in the system" |
32
+ | `wardrobe users by-phone <number>` | The account(s) whose **own** phone is that number (`+972…`, `05…`, `052-…` all match) |
33
+ | `wardrobe orders set-status <id> <status> [--dry-run] [--note <text>]` | **Office accounts only** (`orders:transition`, admin/support approvers). Moves the order one legal step: `paid`, `pending_measurement`, `sent_to_manufacturing`, `pending_assembly`, `provided` — never `cancelled`. Already there → "nothing changed"; an illegal step answers 409 with the reason (e.g. `pending_assembly` is only reachable from `in_manufacturing` when transport & installation was bought) — report that to a human, don't retry. `--dry-run` checks without changing; `--note` puts a receipt number on the order's history |
34
+
35
+ `projects list` returns each project's **customer and owning designer inline** — so "who
36
+ is the customer on each of this designer's projects" is one call with `--designer`, never
37
+ one call per project.
38
+
39
+ `users list` takes the same `--limit` (1–100, default 20) / `--since` paging as the other
40
+ cross-project lists; a malformed value is a `400`, not an ignored filter.
41
+
42
+ ## `wardrobe users` is a directory lookup, not a customer lookup
43
+
44
+ `users list` / `users by-phone` read the `User` **account** table — designers, admins,
45
+ support and self-serve clients who can sign in — not the per-project `Customer` contact
46
+ card `wardrobe customers` reads. Each row carries the account's own `phone` (empty until
47
+ that user passes the onboarding phone step; staff never do). `users by-phone` matches
48
+ **only** that field: a customer's number never resolves to the designer who serves them,
49
+ so its answer is "who is this person", safe to act on as the caller's identity. Numbers
50
+ match as the national number, so `+972526665153` and `0526665153` are the same. More than
51
+ one row means the number is ambiguous (the field is self-declared, not unique) — say so
52
+ rather than picking one. Zero rows is normal and not an error; for the customer side of
53
+ the same number use `wardrobe projects list --phone <number>`.
54
+
55
+ ## Still not reachable, even for staff
56
+
57
+ Anything under `/admin` (catalog, rates, users' roles, commissions), invitations and the
58
+ manufacturing-files export stay in the web app — every agent key is barred from them
59
+ (`403 agent_not_permitted`), whatever its scopes.