niiko-cli 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +17 -0
- package/README.md +89 -31
- package/dist/niiko.js +155 -155
- package/package.json +18 -5
package/LICENSE
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vorluno
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
|
6
|
+
documentation files (the "Software"), to deal in the Software without restriction, including without limitation
|
|
7
|
+
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
|
|
8
|
+
to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions
|
|
11
|
+
of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
|
14
|
+
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
15
|
+
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
|
16
|
+
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
17
|
+
DEALINGS IN THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,39 +1,97 @@
|
|
|
1
1
|
# niiko-cli
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
The niiko actions API from the terminal. One binary, four commands, and the outcome in the exit code.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/niiko-cli) [](https://nodejs.org)   [](https://developers.niiko.org)
|
|
6
|
+
|
|
7
|
+
**A CLI that knows the actions it was written for is stale the day a new one opens.** This one is generated
|
|
8
|
+
from the same plan as the SDKs, the reference and the MCP server (plan `6c3240a7b22d`): when an action opens,
|
|
9
|
+
`niiko actions` lists it and `niiko run` accepts it — nobody edits this repository by hand.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install -g niiko-cli
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Node 20 or newer. No runtime dependencies — `fetch`, `fs` and `os` ship with Node.
|
|
18
|
+
|
|
19
|
+
## Use
|
|
5
20
|
|
|
6
21
|
```bash
|
|
7
|
-
|
|
8
|
-
niiko
|
|
9
|
-
niiko actions # las acciones abiertas, con su propósito
|
|
22
|
+
niiko login --key nk_… # saves the key (mode 0600) to ~/.config/niiko/config.json
|
|
23
|
+
niiko actions # what is open today, with its purpose
|
|
10
24
|
niiko describe miira.lead_create
|
|
11
25
|
niiko run miira.lead_create --input '{ … }'
|
|
12
26
|
```
|
|
13
27
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
28
|
+
Output is always JSON. **The outcome is the exit code**, so a script never has to parse anything:
|
|
29
|
+
|
|
30
|
+
| Exit code | Meaning |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `0` | `done` — the action happened; `output` is in stdout. |
|
|
33
|
+
| `2` | `refused` — it did not happen; `reason` names why and `detail.message` says what to do. |
|
|
34
|
+
| `3` | `pending_approval` — it is waiting for a human signature and may complete hours later. |
|
|
35
|
+
| `1` | An error of the CLI itself or of the transport. |
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
niiko run crm.note_added --input '{ "client": "Casa Mestiza", "note": "Prefers WhatsApp." }' && echo "saved"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
> [!NOTE]
|
|
42
|
+
> Every `run` sends an `Idempotency-Key` (or the one you pass with `--idempotency-key`), and echoes it in the
|
|
43
|
+
> output even when the server does not. Retrying the same key never runs the action twice — so if the terminal
|
|
44
|
+
> died before you read the reply, run it again with the key you got.
|
|
45
|
+
|
|
46
|
+
The key is also read from `NIIKO_API_KEY` and the URL from `NIIKO_URL`, in that order after `--key`/`--url`,
|
|
47
|
+
for scripts that must not touch a person's configuration. The key is never printed.
|
|
48
|
+
|
|
49
|
+
## The actions today (13)
|
|
50
|
+
|
|
51
|
+
| Action | What it does |
|
|
52
|
+
|---|---|
|
|
53
|
+
| `miira.lead_create` | Creates a new lead in the workspace CRM from its contact details. If a matching one already exists it is not duplicated: the reply is `ambiguous` with the candidates. |
|
|
54
|
+
| `crm.call_logged` | Logs what was discussed in a call on a client's record, naming the client; optionally creates the follow-up with its date. Reads nothing and calls no one. |
|
|
55
|
+
| `crm.owner_assigned` | Changes who owns a client, naming the client and the team member (by name or email). If either is ambiguous it refuses with the list. |
|
|
56
|
+
| `crm.stage_moved` | Moves a client's open deal to another pipeline stage, naming the client and the stage. Creates no deals: with no open deal it refuses, and with several it refuses with the list. |
|
|
57
|
+
| `kiipu.invoice_proposed` | Prepares an invoice as a DRAFT for a client named by name, with its lines and taxes. Does NOT issue it, does NOT number it and does NOT count as debt: a person reviews and issues it in Kiipu. Does not create the client if it does not exist. |
|
|
58
|
+
| `crm.task_created` | Creates a reminder (a task with date and time) on a client named by name. Does not log a call: that is crm.call_logged. Does not create the client if it does not exist. |
|
|
59
|
+
| `crm.deal_created` | Opens a new deal in the pipeline for a client named by name, with a title, an optional value in USD, an optional stage (by name; without it, the first one) and an optional owner. Does not check for other open deals: it returns how many remain so a duplicate is visible. Does not win or lose it: that is crm.stage_moved. |
|
|
60
|
+
| `crm.note_added` | Saves a note on a client's record, naming the client: something to know next time, with no call and no date. For a call use crm.call_logged; for a dated reminder, crm.task_created. |
|
|
61
|
+
| `crm.contact_added` | Adds a person (name, and optionally email, phone and role) to a client's record, naming the client. Does not make them the primary contact and does not create the client. If someone with that email or phone already existed, the reply says so but does not block it. |
|
|
62
|
+
| `miira.broadcast_quoted` | Quotes sending the SAME WhatsApp message to several clients named by name (up to 50). Sends NOTHING: per client, it says whether the text goes as-is (24-hour window open, free), whether an approved template is needed and what it costs, or why that client cannot be messaged. Returns a signed quote valid for 15 minutes; to send, call miira.broadcast_sent with it. Show the quote to the person first. |
|
|
63
|
+
| `miira.broadcast_sent` | Sends the WhatsApp broadcast quoted by miira.broadcast_quoted, exactly to whom and how the quote said. If anything changed (window, consent, rate) it refuses with a new quote to confirm again. Costs money when templates are involved: do not call it without the person having seen the cost. |
|
|
64
|
+
| `kiipu.draft_voided` | Voids a DRAFT invoice (one created with kiipu.invoice_proposed and not yet issued), by its id or by the client's name when it is their only draft. Does not void issued invoices: that is for a person in Kiipu. |
|
|
65
|
+
| `kiipu.payment_reported` | Leaves in the Kiipu approval queue the notice that a client (by name) paid a given amount of an open invoice. Does NOT apply the payment and touches no balances: a person checks it against the bank and applies it. If the client has several open invoices the number must be given. |
|
|
66
|
+
|
|
67
|
+
> [!IMPORTANT]
|
|
68
|
+
> Each one needs **two** permissions: a key with that action's scope, **and** the workspace owner having switched
|
|
69
|
+
> the permission on (Administration → Permissions). Holding the key is not enough — they are two decisions made by
|
|
70
|
+
> different people.
|
|
71
|
+
|
|
72
|
+
## What it does not do
|
|
73
|
+
|
|
74
|
+
| Not included | Why |
|
|
75
|
+
|---|---|
|
|
76
|
+
| **Client-side validation** | The server validates, and its refusal travels with a name. A client that validates differently is the drift all of this avoids. |
|
|
77
|
+
| **Reads or pagination** | This API exposes verbs, not rows. There is nothing to list. |
|
|
78
|
+
| **OAuth** | The CLI belongs to whoever holds a key. AI clients connect through the [MCP server](https://github.com/vorluno/niiko-mcp-server) instead. |
|
|
79
|
+
|
|
80
|
+
## Support
|
|
81
|
+
|
|
82
|
+
| | |
|
|
83
|
+
|---|---|
|
|
84
|
+
| **Versioning** | Semantic. Every visible change is in the [changelog](https://developers.niiko.org/#changelog) of the API reference. |
|
|
85
|
+
| **Regeneration** | This repository is generated from the niiko action manifest. When an action opens or a contract changes, a new version is published — nothing here is edited by hand. |
|
|
86
|
+
| **Issues** | [vorluno/niiko-cli/issues](https://github.com/vorluno/niiko-cli/issues) — a fault of the API itself starts at [developers.niiko.org](https://developers.niiko.org). |
|
|
87
|
+
| **Security** | `security@vorluno.dev` — first response within 48 hours, patch or plan within 7 days. |
|
|
88
|
+
|
|
89
|
+
## Related
|
|
90
|
+
|
|
91
|
+
- **[@vorluno/niiko-sdk](https://github.com/vorluno/niiko-sdk-typescript)** — the TypeScript client, same plan.
|
|
92
|
+
- **[niiko (Python)](https://github.com/vorluno/niiko-sdk-python)** — the Python client, same plan.
|
|
93
|
+
- **[n8n-nodes-niiko](https://github.com/vorluno/n8n-nodes-niiko)** — the same actions as an n8n node.
|
|
94
|
+
- **[niiko-mcp-server](https://github.com/vorluno/niiko-mcp-server)** — the same actions as MCP tools, for Claude and other AI clients.
|
|
95
|
+
- **[developers.niiko.org](https://developers.niiko.org)** — the API reference this is generated alongside.
|
|
96
|
+
|
|
97
|
+
<sub>Built and maintained by <a href="https://vorluno.dev">Vorluno</a>, a software studio in Panama, and generated from <a href="https://niiko.org">niiko</a>'s production action manifest — the same one the server enforces. MIT.</sub>
|
package/dist/niiko.js
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
3
|
-
// Plan `
|
|
2
|
+
// GENERATED by `tooling/build-cli.ts` in the niiko monorepo — DO NOT EDIT BY HAND.
|
|
3
|
+
// Plan `6c3240a7b22d`. A new action shows up here on regeneration; none is added by editing this file.
|
|
4
4
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { join } from "node:path";
|
|
7
|
-
/**
|
|
8
|
-
const
|
|
7
|
+
/** The catalog, exactly as it was in plan `6c3240a7b22d`. */
|
|
8
|
+
const CATALOG = [
|
|
9
9
|
{
|
|
10
|
-
"
|
|
10
|
+
"action": "miira.lead_create",
|
|
11
11
|
"version": 1,
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
12
|
+
"scope": "miira.lead_create@1",
|
|
13
|
+
"purpose": "Creates a new lead in the workspace CRM from its contact details. If a matching one already exists it is not duplicated: the reply is `ambiguous` with the candidates.",
|
|
14
|
+
"refusalReasons": [
|
|
15
15
|
"honeypot",
|
|
16
16
|
"invalid_email",
|
|
17
17
|
"disposable_email",
|
|
18
18
|
"invalid_identity"
|
|
19
19
|
],
|
|
20
|
-
"
|
|
20
|
+
"input": {
|
|
21
21
|
"type": "object",
|
|
22
22
|
"properties": {
|
|
23
23
|
"submissionId": {
|
|
@@ -84,7 +84,7 @@ const CATALOGO = [
|
|
|
84
84
|
"source"
|
|
85
85
|
]
|
|
86
86
|
},
|
|
87
|
-
"
|
|
87
|
+
"output": {
|
|
88
88
|
"type": "object",
|
|
89
89
|
"properties": {
|
|
90
90
|
"outcome": {
|
|
@@ -131,17 +131,17 @@ const CATALOGO = [
|
|
|
131
131
|
}
|
|
132
132
|
},
|
|
133
133
|
{
|
|
134
|
-
"
|
|
134
|
+
"action": "crm.call_logged",
|
|
135
135
|
"version": 1,
|
|
136
|
-
"
|
|
137
|
-
"
|
|
138
|
-
"
|
|
136
|
+
"scope": "crm.call_logged@1",
|
|
137
|
+
"purpose": "Logs what was discussed in a call on a client's record, naming the client; optionally creates the follow-up with its date. Reads nothing and calls no one.",
|
|
138
|
+
"refusalReasons": [
|
|
139
139
|
"cliente_no_encontrado",
|
|
140
140
|
"cliente_ambiguo",
|
|
141
141
|
"demasiados_clientes",
|
|
142
142
|
"sin_permiso"
|
|
143
143
|
],
|
|
144
|
-
"
|
|
144
|
+
"input": {
|
|
145
145
|
"type": "object",
|
|
146
146
|
"properties": {
|
|
147
147
|
"client": {
|
|
@@ -178,7 +178,7 @@ const CATALOGO = [
|
|
|
178
178
|
"summary"
|
|
179
179
|
]
|
|
180
180
|
},
|
|
181
|
-
"
|
|
181
|
+
"output": {
|
|
182
182
|
"type": "object",
|
|
183
183
|
"properties": {
|
|
184
184
|
"clientId": {
|
|
@@ -210,11 +210,11 @@ const CATALOGO = [
|
|
|
210
210
|
}
|
|
211
211
|
},
|
|
212
212
|
{
|
|
213
|
-
"
|
|
213
|
+
"action": "crm.owner_assigned",
|
|
214
214
|
"version": 1,
|
|
215
|
-
"
|
|
216
|
-
"
|
|
217
|
-
"
|
|
215
|
+
"scope": "crm.owner_assigned@1",
|
|
216
|
+
"purpose": "Changes who owns a client, naming the client and the team member (by name or email). If either is ambiguous it refuses with the list.",
|
|
217
|
+
"refusalReasons": [
|
|
218
218
|
"cliente_no_encontrado",
|
|
219
219
|
"cliente_ambiguo",
|
|
220
220
|
"demasiados_clientes",
|
|
@@ -222,7 +222,7 @@ const CATALOGO = [
|
|
|
222
222
|
"persona_ambigua",
|
|
223
223
|
"sin_permiso"
|
|
224
224
|
],
|
|
225
|
-
"
|
|
225
|
+
"input": {
|
|
226
226
|
"type": "object",
|
|
227
227
|
"properties": {
|
|
228
228
|
"client": {
|
|
@@ -241,7 +241,7 @@ const CATALOGO = [
|
|
|
241
241
|
"owner"
|
|
242
242
|
]
|
|
243
243
|
},
|
|
244
|
-
"
|
|
244
|
+
"output": {
|
|
245
245
|
"type": "object",
|
|
246
246
|
"properties": {
|
|
247
247
|
"clientId": {
|
|
@@ -266,11 +266,11 @@ const CATALOGO = [
|
|
|
266
266
|
}
|
|
267
267
|
},
|
|
268
268
|
{
|
|
269
|
-
"
|
|
269
|
+
"action": "crm.stage_moved",
|
|
270
270
|
"version": 1,
|
|
271
|
-
"
|
|
272
|
-
"
|
|
273
|
-
"
|
|
271
|
+
"scope": "crm.stage_moved@1",
|
|
272
|
+
"purpose": "Moves a client's open deal to another pipeline stage, naming the client and the stage. Creates no deals: with no open deal it refuses, and with several it refuses with the list.",
|
|
273
|
+
"refusalReasons": [
|
|
274
274
|
"cliente_no_encontrado",
|
|
275
275
|
"cliente_ambiguo",
|
|
276
276
|
"demasiados_clientes",
|
|
@@ -281,7 +281,7 @@ const CATALOGO = [
|
|
|
281
281
|
"ya_en_esa_etapa",
|
|
282
282
|
"sin_permiso"
|
|
283
283
|
],
|
|
284
|
-
"
|
|
284
|
+
"input": {
|
|
285
285
|
"type": "object",
|
|
286
286
|
"properties": {
|
|
287
287
|
"client": {
|
|
@@ -304,7 +304,7 @@ const CATALOGO = [
|
|
|
304
304
|
"stage"
|
|
305
305
|
]
|
|
306
306
|
},
|
|
307
|
-
"
|
|
307
|
+
"output": {
|
|
308
308
|
"type": "object",
|
|
309
309
|
"properties": {
|
|
310
310
|
"dealId": {
|
|
@@ -337,17 +337,17 @@ const CATALOGO = [
|
|
|
337
337
|
}
|
|
338
338
|
},
|
|
339
339
|
{
|
|
340
|
-
"
|
|
340
|
+
"action": "kiipu.invoice_proposed",
|
|
341
341
|
"version": 1,
|
|
342
|
-
"
|
|
343
|
-
"
|
|
344
|
-
"
|
|
342
|
+
"scope": "kiipu.invoice_proposed@1",
|
|
343
|
+
"purpose": "Prepares an invoice as a DRAFT for a client named by name, with its lines and taxes. Does NOT issue it, does NOT number it and does NOT count as debt: a person reviews and issues it in Kiipu. Does not create the client if it does not exist.",
|
|
344
|
+
"refusalReasons": [
|
|
345
345
|
"cliente_no_encontrado",
|
|
346
346
|
"cliente_ambiguo",
|
|
347
347
|
"demasiados_clientes",
|
|
348
348
|
"sin_permiso"
|
|
349
349
|
],
|
|
350
|
-
"
|
|
350
|
+
"input": {
|
|
351
351
|
"type": "object",
|
|
352
352
|
"properties": {
|
|
353
353
|
"client": {
|
|
@@ -406,7 +406,7 @@ const CATALOGO = [
|
|
|
406
406
|
"lines"
|
|
407
407
|
]
|
|
408
408
|
},
|
|
409
|
-
"
|
|
409
|
+
"output": {
|
|
410
410
|
"type": "object",
|
|
411
411
|
"properties": {
|
|
412
412
|
"invoiceId": {
|
|
@@ -455,17 +455,17 @@ const CATALOGO = [
|
|
|
455
455
|
}
|
|
456
456
|
},
|
|
457
457
|
{
|
|
458
|
-
"
|
|
458
|
+
"action": "crm.task_created",
|
|
459
459
|
"version": 1,
|
|
460
|
-
"
|
|
461
|
-
"
|
|
462
|
-
"
|
|
460
|
+
"scope": "crm.task_created@1",
|
|
461
|
+
"purpose": "Creates a reminder (a task with date and time) on a client named by name. Does not log a call: that is crm.call_logged. Does not create the client if it does not exist.",
|
|
462
|
+
"refusalReasons": [
|
|
463
463
|
"cliente_no_encontrado",
|
|
464
464
|
"cliente_ambiguo",
|
|
465
465
|
"demasiados_clientes",
|
|
466
466
|
"sin_permiso"
|
|
467
467
|
],
|
|
468
|
-
"
|
|
468
|
+
"input": {
|
|
469
469
|
"type": "object",
|
|
470
470
|
"properties": {
|
|
471
471
|
"client": {
|
|
@@ -489,7 +489,7 @@ const CATALOGO = [
|
|
|
489
489
|
"dueAt"
|
|
490
490
|
]
|
|
491
491
|
},
|
|
492
|
-
"
|
|
492
|
+
"output": {
|
|
493
493
|
"type": "object",
|
|
494
494
|
"properties": {
|
|
495
495
|
"clientId": {
|
|
@@ -514,11 +514,11 @@ const CATALOGO = [
|
|
|
514
514
|
}
|
|
515
515
|
},
|
|
516
516
|
{
|
|
517
|
-
"
|
|
517
|
+
"action": "crm.deal_created",
|
|
518
518
|
"version": 1,
|
|
519
|
-
"
|
|
520
|
-
"
|
|
521
|
-
"
|
|
519
|
+
"scope": "crm.deal_created@1",
|
|
520
|
+
"purpose": "Opens a new deal in the pipeline for a client named by name, with a title, an optional value in USD, an optional stage (by name; without it, the first one) and an optional owner. Does not check for other open deals: it returns how many remain so a duplicate is visible. Does not win or lose it: that is crm.stage_moved.",
|
|
521
|
+
"refusalReasons": [
|
|
522
522
|
"cliente_no_encontrado",
|
|
523
523
|
"cliente_ambiguo",
|
|
524
524
|
"demasiados_clientes",
|
|
@@ -528,7 +528,7 @@ const CATALOGO = [
|
|
|
528
528
|
"persona_ambigua",
|
|
529
529
|
"sin_permiso"
|
|
530
530
|
],
|
|
531
|
-
"
|
|
531
|
+
"input": {
|
|
532
532
|
"type": "object",
|
|
533
533
|
"properties": {
|
|
534
534
|
"client": {
|
|
@@ -561,7 +561,7 @@ const CATALOGO = [
|
|
|
561
561
|
"title"
|
|
562
562
|
]
|
|
563
563
|
},
|
|
564
|
-
"
|
|
564
|
+
"output": {
|
|
565
565
|
"type": "object",
|
|
566
566
|
"properties": {
|
|
567
567
|
"dealId": {
|
|
@@ -617,17 +617,17 @@ const CATALOGO = [
|
|
|
617
617
|
}
|
|
618
618
|
},
|
|
619
619
|
{
|
|
620
|
-
"
|
|
620
|
+
"action": "crm.note_added",
|
|
621
621
|
"version": 1,
|
|
622
|
-
"
|
|
623
|
-
"
|
|
624
|
-
"
|
|
622
|
+
"scope": "crm.note_added@1",
|
|
623
|
+
"purpose": "Saves a note on a client's record, naming the client: something to know next time, with no call and no date. For a call use crm.call_logged; for a dated reminder, crm.task_created.",
|
|
624
|
+
"refusalReasons": [
|
|
625
625
|
"cliente_no_encontrado",
|
|
626
626
|
"cliente_ambiguo",
|
|
627
627
|
"demasiados_clientes",
|
|
628
628
|
"sin_permiso"
|
|
629
629
|
],
|
|
630
|
-
"
|
|
630
|
+
"input": {
|
|
631
631
|
"type": "object",
|
|
632
632
|
"properties": {
|
|
633
633
|
"client": {
|
|
@@ -646,7 +646,7 @@ const CATALOGO = [
|
|
|
646
646
|
"note"
|
|
647
647
|
]
|
|
648
648
|
},
|
|
649
|
-
"
|
|
649
|
+
"output": {
|
|
650
650
|
"type": "object",
|
|
651
651
|
"properties": {
|
|
652
652
|
"clientId": {
|
|
@@ -667,17 +667,17 @@ const CATALOGO = [
|
|
|
667
667
|
}
|
|
668
668
|
},
|
|
669
669
|
{
|
|
670
|
-
"
|
|
670
|
+
"action": "crm.contact_added",
|
|
671
671
|
"version": 1,
|
|
672
|
-
"
|
|
673
|
-
"
|
|
674
|
-
"
|
|
672
|
+
"scope": "crm.contact_added@1",
|
|
673
|
+
"purpose": "Adds a person (name, and optionally email, phone and role) to a client's record, naming the client. Does not make them the primary contact and does not create the client. If someone with that email or phone already existed, the reply says so but does not block it.",
|
|
674
|
+
"refusalReasons": [
|
|
675
675
|
"cliente_no_encontrado",
|
|
676
676
|
"cliente_ambiguo",
|
|
677
677
|
"demasiados_clientes",
|
|
678
678
|
"sin_permiso"
|
|
679
679
|
],
|
|
680
|
-
"
|
|
680
|
+
"input": {
|
|
681
681
|
"type": "object",
|
|
682
682
|
"properties": {
|
|
683
683
|
"client": {
|
|
@@ -711,7 +711,7 @@ const CATALOGO = [
|
|
|
711
711
|
"name"
|
|
712
712
|
]
|
|
713
713
|
},
|
|
714
|
-
"
|
|
714
|
+
"output": {
|
|
715
715
|
"type": "object",
|
|
716
716
|
"properties": {
|
|
717
717
|
"clientId": {
|
|
@@ -736,16 +736,16 @@ const CATALOGO = [
|
|
|
736
736
|
}
|
|
737
737
|
},
|
|
738
738
|
{
|
|
739
|
-
"
|
|
739
|
+
"action": "miira.broadcast_quoted",
|
|
740
740
|
"version": 1,
|
|
741
|
-
"
|
|
742
|
-
"
|
|
743
|
-
"
|
|
741
|
+
"scope": "miira.broadcast_quoted@1",
|
|
742
|
+
"purpose": "Quotes sending the SAME WhatsApp message to several clients named by name (up to 50). Sends NOTHING: per client, it says whether the text goes as-is (24-hour window open, free), whether an approved template is needed and what it costs, or why that client cannot be messaged. Returns a signed quote valid for 15 minutes; to send, call miira.broadcast_sent with it. Show the quote to the person first.",
|
|
743
|
+
"refusalReasons": [
|
|
744
744
|
"plantilla_invalida",
|
|
745
745
|
"tarifas_vencidas",
|
|
746
746
|
"sin_permiso"
|
|
747
747
|
],
|
|
748
|
-
"
|
|
748
|
+
"input": {
|
|
749
749
|
"type": "object",
|
|
750
750
|
"properties": {
|
|
751
751
|
"clients": {
|
|
@@ -779,7 +779,7 @@ const CATALOGO = [
|
|
|
779
779
|
"message"
|
|
780
780
|
]
|
|
781
781
|
},
|
|
782
|
-
"
|
|
782
|
+
"output": {
|
|
783
783
|
"type": "object",
|
|
784
784
|
"properties": {
|
|
785
785
|
"rows": {
|
|
@@ -907,11 +907,11 @@ const CATALOGO = [
|
|
|
907
907
|
}
|
|
908
908
|
},
|
|
909
909
|
{
|
|
910
|
-
"
|
|
910
|
+
"action": "miira.broadcast_sent",
|
|
911
911
|
"version": 1,
|
|
912
|
-
"
|
|
913
|
-
"
|
|
914
|
-
"
|
|
912
|
+
"scope": "miira.broadcast_sent@1",
|
|
913
|
+
"purpose": "Sends the WhatsApp broadcast quoted by miira.broadcast_quoted, exactly to whom and how the quote said. If anything changed (window, consent, rate) it refuses with a new quote to confirm again. Costs money when templates are involved: do not call it without the person having seen the cost.",
|
|
914
|
+
"refusalReasons": [
|
|
915
915
|
"presupuesto_invalido",
|
|
916
916
|
"presupuesto_vencido",
|
|
917
917
|
"presupuesto_cambiado",
|
|
@@ -919,7 +919,7 @@ const CATALOGO = [
|
|
|
919
919
|
"tarifas_vencidas",
|
|
920
920
|
"sin_permiso"
|
|
921
921
|
],
|
|
922
|
-
"
|
|
922
|
+
"input": {
|
|
923
923
|
"type": "object",
|
|
924
924
|
"properties": {
|
|
925
925
|
"quote": {
|
|
@@ -932,7 +932,7 @@ const CATALOGO = [
|
|
|
932
932
|
"quote"
|
|
933
933
|
]
|
|
934
934
|
},
|
|
935
|
-
"
|
|
935
|
+
"output": {
|
|
936
936
|
"type": "object",
|
|
937
937
|
"properties": {
|
|
938
938
|
"queued": {
|
|
@@ -977,11 +977,11 @@ const CATALOGO = [
|
|
|
977
977
|
}
|
|
978
978
|
},
|
|
979
979
|
{
|
|
980
|
-
"
|
|
980
|
+
"action": "kiipu.draft_voided",
|
|
981
981
|
"version": 1,
|
|
982
|
-
"
|
|
983
|
-
"
|
|
984
|
-
"
|
|
982
|
+
"scope": "kiipu.draft_voided@1",
|
|
983
|
+
"purpose": "Voids a DRAFT invoice (one created with kiipu.invoice_proposed and not yet issued), by its id or by the client's name when it is their only draft. Does not void issued invoices: that is for a person in Kiipu.",
|
|
984
|
+
"refusalReasons": [
|
|
985
985
|
"cliente_no_encontrado",
|
|
986
986
|
"cliente_ambiguo",
|
|
987
987
|
"demasiados_clientes",
|
|
@@ -990,7 +990,7 @@ const CATALOGO = [
|
|
|
990
990
|
"no_es_borrador",
|
|
991
991
|
"sin_permiso"
|
|
992
992
|
],
|
|
993
|
-
"
|
|
993
|
+
"input": {
|
|
994
994
|
"type": "object",
|
|
995
995
|
"properties": {
|
|
996
996
|
"invoiceId": {
|
|
@@ -1005,7 +1005,7 @@ const CATALOGO = [
|
|
|
1005
1005
|
},
|
|
1006
1006
|
"required": []
|
|
1007
1007
|
},
|
|
1008
|
-
"
|
|
1008
|
+
"output": {
|
|
1009
1009
|
"type": "object",
|
|
1010
1010
|
"properties": {
|
|
1011
1011
|
"invoiceId": {
|
|
@@ -1034,11 +1034,11 @@ const CATALOGO = [
|
|
|
1034
1034
|
}
|
|
1035
1035
|
},
|
|
1036
1036
|
{
|
|
1037
|
-
"
|
|
1037
|
+
"action": "kiipu.payment_reported",
|
|
1038
1038
|
"version": 1,
|
|
1039
|
-
"
|
|
1040
|
-
"
|
|
1041
|
-
"
|
|
1039
|
+
"scope": "kiipu.payment_reported@1",
|
|
1040
|
+
"purpose": "Leaves in the Kiipu approval queue the notice that a client (by name) paid a given amount of an open invoice. Does NOT apply the payment and touches no balances: a person checks it against the bank and applies it. If the client has several open invoices the number must be given.",
|
|
1041
|
+
"refusalReasons": [
|
|
1042
1042
|
"cliente_no_encontrado",
|
|
1043
1043
|
"cliente_ambiguo",
|
|
1044
1044
|
"demasiados_clientes",
|
|
@@ -1047,7 +1047,7 @@ const CATALOGO = [
|
|
|
1047
1047
|
"factura_no_encontrada",
|
|
1048
1048
|
"sin_permiso"
|
|
1049
1049
|
],
|
|
1050
|
-
"
|
|
1050
|
+
"input": {
|
|
1051
1051
|
"type": "object",
|
|
1052
1052
|
"properties": {
|
|
1053
1053
|
"client": {
|
|
@@ -1070,7 +1070,7 @@ const CATALOGO = [
|
|
|
1070
1070
|
"amountUsd"
|
|
1071
1071
|
]
|
|
1072
1072
|
},
|
|
1073
|
-
"
|
|
1073
|
+
"output": {
|
|
1074
1074
|
"type": "object",
|
|
1075
1075
|
"properties": {
|
|
1076
1076
|
"submissionId": {
|
|
@@ -1115,33 +1115,33 @@ const CATALOGO = [
|
|
|
1115
1115
|
}
|
|
1116
1116
|
}
|
|
1117
1117
|
];
|
|
1118
|
-
const
|
|
1119
|
-
function
|
|
1120
|
-
if (!existsSync(
|
|
1118
|
+
const CONFIG_PATH = join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "niiko", "config.json");
|
|
1119
|
+
function readConfig() {
|
|
1120
|
+
if (!existsSync(CONFIG_PATH))
|
|
1121
1121
|
return null;
|
|
1122
1122
|
try {
|
|
1123
|
-
const c = JSON.parse(readFileSync(
|
|
1123
|
+
const c = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
|
|
1124
1124
|
return typeof c.url === "string" && typeof c.apiKey === "string" ? { url: c.url, apiKey: c.apiKey } : null;
|
|
1125
1125
|
}
|
|
1126
1126
|
catch {
|
|
1127
1127
|
return null;
|
|
1128
1128
|
}
|
|
1129
1129
|
}
|
|
1130
|
-
/**
|
|
1131
|
-
*
|
|
1132
|
-
function
|
|
1133
|
-
const
|
|
1134
|
-
const url = args.get("url") ?? process.env.NIIKO_URL ??
|
|
1135
|
-
const apiKey = args.get("key") ?? process.env.NIIKO_API_KEY ??
|
|
1130
|
+
/** The key: `--key`, then `NIIKO_API_KEY`, then the saved one. In that order, so a script can send without
|
|
1131
|
+
* touching the person's configuration. It is never printed. */
|
|
1132
|
+
function credentials(args) {
|
|
1133
|
+
const saved = readConfig();
|
|
1134
|
+
const url = args.get("url") ?? process.env.NIIKO_URL ?? saved?.url ?? "https://niiko.org";
|
|
1135
|
+
const apiKey = args.get("key") ?? process.env.NIIKO_API_KEY ?? saved?.apiKey;
|
|
1136
1136
|
return apiKey ? { url: url.replace(/\/+$/, ""), apiKey } : null;
|
|
1137
1137
|
}
|
|
1138
|
-
function
|
|
1139
|
-
process.stdout.write(JSON.stringify(
|
|
1140
|
-
process.exit(
|
|
1138
|
+
function exit(code, body) {
|
|
1139
|
+
process.stdout.write(JSON.stringify(body, null, 2) + "\n");
|
|
1140
|
+
process.exit(code);
|
|
1141
1141
|
}
|
|
1142
|
-
/** `--a b`
|
|
1143
|
-
function
|
|
1144
|
-
const
|
|
1142
|
+
/** `--a b` and `--a=b`. Anything not starting with `--` is positional. */
|
|
1143
|
+
function parse(argv) {
|
|
1144
|
+
const positional = [];
|
|
1145
1145
|
const args = new Map();
|
|
1146
1146
|
for (let i = 0; i < argv.length; i++) {
|
|
1147
1147
|
const a = argv[i];
|
|
@@ -1155,109 +1155,109 @@ function parsear(argv) {
|
|
|
1155
1155
|
args.set(k, "");
|
|
1156
1156
|
}
|
|
1157
1157
|
else
|
|
1158
|
-
|
|
1158
|
+
positional.push(a);
|
|
1159
1159
|
}
|
|
1160
|
-
return {
|
|
1160
|
+
return { positional, args };
|
|
1161
1161
|
}
|
|
1162
|
-
function
|
|
1163
|
-
const
|
|
1164
|
-
let
|
|
1165
|
-
if (
|
|
1162
|
+
function readInput(args) {
|
|
1163
|
+
const raw = args.get("input");
|
|
1164
|
+
let text;
|
|
1165
|
+
if (raw === undefined) {
|
|
1166
1166
|
if (process.stdin.isTTY)
|
|
1167
|
-
|
|
1168
|
-
|
|
1167
|
+
exit(1, { error: "missing input: --input '{…}', --input @file, or stdin" });
|
|
1168
|
+
text = readFileSync(0, "utf8");
|
|
1169
1169
|
}
|
|
1170
|
-
else if (
|
|
1171
|
-
|
|
1170
|
+
else if (raw.startsWith("@"))
|
|
1171
|
+
text = readFileSync(raw.slice(1), "utf8");
|
|
1172
1172
|
else
|
|
1173
|
-
|
|
1173
|
+
text = raw;
|
|
1174
1174
|
try {
|
|
1175
|
-
return JSON.parse(
|
|
1175
|
+
return JSON.parse(text);
|
|
1176
1176
|
}
|
|
1177
1177
|
catch {
|
|
1178
|
-
return
|
|
1178
|
+
return exit(1, { error: "input is not valid JSON" });
|
|
1179
1179
|
}
|
|
1180
1180
|
}
|
|
1181
1181
|
function uuid() {
|
|
1182
1182
|
return globalThis.crypto.randomUUID();
|
|
1183
1183
|
}
|
|
1184
|
-
const
|
|
1184
|
+
const HELP = `niiko — the niiko actions API from the terminal (plan 6c3240a7b22d)
|
|
1185
1185
|
|
|
1186
|
-
niiko login --url https://niiko.org --key nk_…
|
|
1187
|
-
niiko actions
|
|
1188
|
-
niiko describe <
|
|
1189
|
-
niiko run <
|
|
1186
|
+
niiko login --url https://niiko.org --key nk_… saves the key to ${CONFIG_PATH}
|
|
1187
|
+
niiko actions the open actions, with their purpose
|
|
1188
|
+
niiko describe <action> input, output and refusal reasons of one action
|
|
1189
|
+
niiko run <action> --input '{…}' exercises an action (also --input @file or stdin)
|
|
1190
1190
|
[--idempotency-key K] [--key nk_…] [--url …]
|
|
1191
1191
|
|
|
1192
|
-
|
|
1193
|
-
|
|
1192
|
+
Exit codes of \`run\`: 0 done · 2 refused (the reason is in the output) · 3 waiting for a signature · 1 error.
|
|
1193
|
+
The key is also read from NIIKO_API_KEY and the URL from NIIKO_URL.`;
|
|
1194
1194
|
async function main() {
|
|
1195
|
-
const {
|
|
1196
|
-
const cmd =
|
|
1195
|
+
const { positional, args } = parse(process.argv.slice(2));
|
|
1196
|
+
const cmd = positional[0];
|
|
1197
1197
|
if (!cmd || cmd === "help" || args.has("help")) {
|
|
1198
|
-
process.stdout.write(
|
|
1198
|
+
process.stdout.write(HELP + "\n");
|
|
1199
1199
|
process.exit(0);
|
|
1200
1200
|
}
|
|
1201
1201
|
if (cmd === "login") {
|
|
1202
1202
|
const apiKey = args.get("key") ?? process.env.NIIKO_API_KEY;
|
|
1203
1203
|
if (!apiKey)
|
|
1204
|
-
|
|
1204
|
+
exit(1, { error: "missing --key nk_… (or NIIKO_API_KEY)" });
|
|
1205
1205
|
const url = (args.get("url") ?? process.env.NIIKO_URL ?? "https://niiko.org").replace(/\/+$/, "");
|
|
1206
|
-
mkdirSync(join(
|
|
1207
|
-
writeFileSync(
|
|
1208
|
-
//
|
|
1209
|
-
|
|
1206
|
+
mkdirSync(join(CONFIG_PATH, ".."), { recursive: true });
|
|
1207
|
+
writeFileSync(CONFIG_PATH, JSON.stringify({ url, apiKey }, null, 2) + "\n", { mode: 0o600 });
|
|
1208
|
+
// Say WHERE it went and the prefix, never the whole key: a terminal is a history.
|
|
1209
|
+
exit(0, { saved: CONFIG_PATH, url, key: apiKey.slice(0, 7) + "…" });
|
|
1210
1210
|
}
|
|
1211
1211
|
if (cmd === "actions") {
|
|
1212
|
-
|
|
1212
|
+
exit(0, CATALOG.map((a) => ({ action: a.action, version: a.version, scope: a.scope, purpose: a.purpose })));
|
|
1213
1213
|
}
|
|
1214
1214
|
if (cmd === "describe") {
|
|
1215
|
-
const id =
|
|
1216
|
-
const a =
|
|
1215
|
+
const id = positional[1];
|
|
1216
|
+
const a = CATALOG.find((x) => x.action === id);
|
|
1217
1217
|
if (!a)
|
|
1218
|
-
|
|
1219
|
-
|
|
1218
|
+
exit(1, { error: `unknown action \`${id}\``, available: CATALOG.map((x) => x.action) });
|
|
1219
|
+
exit(0, a);
|
|
1220
1220
|
}
|
|
1221
1221
|
if (cmd === "run") {
|
|
1222
|
-
const id =
|
|
1223
|
-
const a =
|
|
1222
|
+
const id = positional[1];
|
|
1223
|
+
const a = CATALOG.find((x) => x.action === id);
|
|
1224
1224
|
if (!a)
|
|
1225
|
-
|
|
1226
|
-
const cred =
|
|
1225
|
+
exit(1, { error: `unknown action \`${id}\``, available: CATALOG.map((x) => x.action) });
|
|
1226
|
+
const cred = credentials(args);
|
|
1227
1227
|
if (!cred)
|
|
1228
|
-
|
|
1229
|
-
const
|
|
1230
|
-
const
|
|
1228
|
+
exit(1, { error: "no key: niiko login --key nk_…, or NIIKO_API_KEY, or --key" });
|
|
1229
|
+
const input = readInput(args);
|
|
1230
|
+
const key = args.get("idempotency-key") ?? uuid();
|
|
1231
1231
|
let r;
|
|
1232
1232
|
try {
|
|
1233
|
-
r = await fetch(`${cred.url}/api/v1/actions/${a.
|
|
1233
|
+
r = await fetch(`${cred.url}/api/v1/actions/${a.action}`, {
|
|
1234
1234
|
method: "POST",
|
|
1235
1235
|
headers: {
|
|
1236
1236
|
Authorization: `Bearer ${cred.apiKey}`,
|
|
1237
1237
|
"Content-Type": "application/json",
|
|
1238
|
-
//
|
|
1239
|
-
"Idempotency-Key":
|
|
1240
|
-
"User-Agent": "niiko-cli/
|
|
1238
|
+
// Always with a key: without one a retry after a timeout duplicates the effect, and the server requires it.
|
|
1239
|
+
"Idempotency-Key": key,
|
|
1240
|
+
"User-Agent": "niiko-cli/0.1.1",
|
|
1241
1241
|
},
|
|
1242
|
-
body: JSON.stringify(
|
|
1242
|
+
body: JSON.stringify(input),
|
|
1243
1243
|
});
|
|
1244
1244
|
}
|
|
1245
1245
|
catch (e) {
|
|
1246
|
-
|
|
1246
|
+
exit(1, { error: "could not connect", detail: String(e), url: cred.url });
|
|
1247
1247
|
}
|
|
1248
|
-
const
|
|
1249
|
-
const status =
|
|
1250
|
-
//
|
|
1251
|
-
//
|
|
1252
|
-
const
|
|
1248
|
+
const body = await r.json().catch(() => null);
|
|
1249
|
+
const status = body?.status;
|
|
1250
|
+
// The idempotency key travels in the output even if the server does not echo it: it is what lets you retry
|
|
1251
|
+
// EXACTLY the same thing if the terminal died before reading the reply.
|
|
1252
|
+
const out = typeof body === "object" && body !== null ? { ...body, idempotencyKey: body.idempotencyKey ?? key } : { http: r.status, body };
|
|
1253
1253
|
if (status === "done")
|
|
1254
|
-
|
|
1254
|
+
exit(0, out);
|
|
1255
1255
|
if (status === "pending_approval")
|
|
1256
|
-
|
|
1256
|
+
exit(3, out);
|
|
1257
1257
|
if (status === "refused")
|
|
1258
|
-
|
|
1259
|
-
|
|
1258
|
+
exit(2, out);
|
|
1259
|
+
exit(1, { error: `unexpected response (HTTP ${r.status})`, ...out });
|
|
1260
1260
|
}
|
|
1261
|
-
|
|
1261
|
+
exit(1, { error: `unknown command: ${cmd}`, help: "niiko help" });
|
|
1262
1262
|
}
|
|
1263
|
-
main().catch((e) =>
|
|
1263
|
+
main().catch((e) => exit(1, { error: String(e) }));
|
package/package.json
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "niiko-cli",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "The niiko actions API from the terminal. Generated from the same plan as the SDKs.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"niiko",
|
|
7
|
+
"cli",
|
|
8
|
+
"crm",
|
|
9
|
+
"whatsapp",
|
|
10
|
+
"invoicing",
|
|
11
|
+
"actions-api"
|
|
12
|
+
],
|
|
5
13
|
"license": "MIT",
|
|
6
14
|
"type": "module",
|
|
7
15
|
"bin": {
|
|
@@ -9,7 +17,8 @@
|
|
|
9
17
|
},
|
|
10
18
|
"files": [
|
|
11
19
|
"dist",
|
|
12
|
-
"README.md"
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE"
|
|
13
22
|
],
|
|
14
23
|
"engines": {
|
|
15
24
|
"node": ">=20"
|
|
@@ -24,7 +33,11 @@
|
|
|
24
33
|
},
|
|
25
34
|
"repository": {
|
|
26
35
|
"type": "git",
|
|
27
|
-
"url": "https://github.com/vorluno/niiko-cli"
|
|
36
|
+
"url": "git+https://github.com/vorluno/niiko-cli.git"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://developers.niiko.org",
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/vorluno/niiko-cli/issues"
|
|
28
41
|
},
|
|
29
|
-
"
|
|
42
|
+
"author": "Vorluno (https://vorluno.dev)"
|
|
30
43
|
}
|