@myapihq/cli 2.14.0 → 2.15.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/dist/commands/feedback.js +75 -18
- package/dist/errors.js +1 -0
- package/dist/skills/my-feedback-api/SKILL.md +34 -15
- package/package.json +2 -2
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// because nothing they read mentioned it.
|
|
7
7
|
import { feedback as sdkFeedback } from '@myapihq/sdk';
|
|
8
8
|
import { requireConfig } from '../config.js';
|
|
9
|
-
import { success, error, info,
|
|
9
|
+
import { success, error, info, printJson, banner } from '../output.js';
|
|
10
10
|
import { formatDate } from '../utils.js';
|
|
11
11
|
import { requireOrg, requireArg, confirmDestructive } from '../helpers.js';
|
|
12
12
|
export const EXPOSES = [
|
|
@@ -20,6 +20,7 @@ export const EXPOSES = [
|
|
|
20
20
|
'DELETE /feedback/orgs/{org_id}/widgets/{id}',
|
|
21
21
|
];
|
|
22
22
|
export const SCHEMA = {
|
|
23
|
+
trace: 'boolean',
|
|
23
24
|
routes: 'string',
|
|
24
25
|
accent: 'string',
|
|
25
26
|
position: 'string',
|
|
@@ -33,6 +34,28 @@ export const SCHEMA = {
|
|
|
33
34
|
};
|
|
34
35
|
// The platform's enum, checked against the schema rather than invented.
|
|
35
36
|
const KINDS = ['bug', 'issue', 'suggestion'];
|
|
37
|
+
// A selector says `div#install > button.save`. The context says the button read
|
|
38
|
+
// "Save settings" and sat under "Install". The second is what a person needs,
|
|
39
|
+
// so prefer it and fall back to the selector only when it is absent.
|
|
40
|
+
function describeTarget(i) {
|
|
41
|
+
const ctx = i.target_context;
|
|
42
|
+
const one = ctx && !ctx.region ? ctx : (ctx?.region?.[0]);
|
|
43
|
+
if (one) {
|
|
44
|
+
const label = one.text || one.aria_label || one.name || one.id;
|
|
45
|
+
const bits = [];
|
|
46
|
+
if (label)
|
|
47
|
+
bits.push(JSON.stringify(label));
|
|
48
|
+
if (one.tag)
|
|
49
|
+
bits.push(`(${one.tag})`);
|
|
50
|
+
let s = bits.join(' ') || one.tag || '';
|
|
51
|
+
if (one.section)
|
|
52
|
+
s += ` under ${JSON.stringify(one.section)}`;
|
|
53
|
+
const more = ctx?.region?.length > 1 ? ` +${ctx.region.length - 1} more` : '';
|
|
54
|
+
if (s)
|
|
55
|
+
return s + more;
|
|
56
|
+
}
|
|
57
|
+
return i.target_selector ?? '';
|
|
58
|
+
}
|
|
36
59
|
export async function list(flags) {
|
|
37
60
|
const config = requireConfig();
|
|
38
61
|
const orgId = requireOrg(flags, config, 'myapi feedback list [--kind <k>] [--status open|resolved] [--org <id>]');
|
|
@@ -56,23 +79,54 @@ export async function list(flags) {
|
|
|
56
79
|
// that explicitly, having been bitten by the opposite in CRM search.
|
|
57
80
|
const shown = page.items?.length ?? 0;
|
|
58
81
|
info(shown === page.total ? `${shown} item${shown === 1 ? '' : 's'}` : `${shown} of ${page.total} items`);
|
|
59
|
-
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
82
|
+
info('');
|
|
83
|
+
// A report used to be one line of prose with a CSS selector under it. It now
|
|
84
|
+
// carries what was pointed at, what happened before, and a picture — and a
|
|
85
|
+
// table cannot hold any of that. Twenty of these dumped in full would be
|
|
86
|
+
// unreadable, so each item summarises and `--trace` expands.
|
|
87
|
+
for (const i of page.items ?? []) {
|
|
88
|
+
const when = i.created_at ? formatDate(i.created_at) : '';
|
|
89
|
+
info(`${i.kind.padEnd(10)} ${when} ${i.route ?? i.page_url ?? ''}`);
|
|
90
|
+
info(` ${JSON.stringify(i.body.length > 100 ? `${i.body.slice(0, 97)}…` : i.body)}`);
|
|
91
|
+
const pointing = describeTarget(i);
|
|
92
|
+
if (pointing)
|
|
93
|
+
info(` pointing at ${pointing}`);
|
|
94
|
+
const trace = Array.isArray(i.trace) ? i.trace : [];
|
|
95
|
+
if (trace.length) {
|
|
96
|
+
// Errors and failed requests are the highest-signal part — for most
|
|
97
|
+
// reports the answer is a 500 nobody saw. Counting them beats truncating
|
|
98
|
+
// the trace into the summary line.
|
|
99
|
+
const errors = trace.filter(e => e.kind === 'error').length;
|
|
100
|
+
const failed = trace.filter(e => e.kind === 'request' && /\b[45]\d\d\b/.test(e.detail)).length;
|
|
101
|
+
const parts = [`${trace.length} step${trace.length === 1 ? '' : 's'}`];
|
|
102
|
+
if (errors)
|
|
103
|
+
parts.push(`${errors} error${errors === 1 ? '' : 's'}`);
|
|
104
|
+
if (failed)
|
|
105
|
+
parts.push(`${failed} failed request${failed === 1 ? '' : 's'}`);
|
|
106
|
+
info(` before ${parts.join(' · ')}`);
|
|
107
|
+
}
|
|
108
|
+
if (i.screenshot_url)
|
|
109
|
+
info(' screenshot yes');
|
|
110
|
+
else if (i.screenshot_asset_id)
|
|
111
|
+
info(' screenshot yes (link could not be signed)');
|
|
112
|
+
if (flags.trace && trace.length) {
|
|
113
|
+
info('');
|
|
114
|
+
for (const e of trace) {
|
|
115
|
+
// `t` is ms since page load, not wall clock — rendered as a delta so
|
|
116
|
+
// nobody reads it as a time of day.
|
|
117
|
+
info(` +${(e.t / 1000).toFixed(1)}s ${String(e.kind).padEnd(8)} ${e.detail}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
info(` id ${i.id}`);
|
|
121
|
+
info('');
|
|
122
|
+
}
|
|
123
|
+
if (!shown)
|
|
124
|
+
info('No feedback yet. Put a widget on a page: myapi feedback widget create <name>');
|
|
73
125
|
const withShots = (page.items ?? []).filter(i => i.screenshot_url || i.screenshot_asset_id).length;
|
|
74
|
-
if (withShots > 0
|
|
75
|
-
info(
|
|
126
|
+
if (withShots > 0)
|
|
127
|
+
info(`\`--json\` carries the screenshot links (signed, ~1h — do not cache them).`);
|
|
128
|
+
if ((page.items ?? []).some(i => Array.isArray(i.trace) && i.trace.length) && !flags.trace) {
|
|
129
|
+
info('`--trace` expands what happened before each report.');
|
|
76
130
|
}
|
|
77
131
|
if (page.has_more)
|
|
78
132
|
info('More available — raise --limit or pass --offset.');
|
|
@@ -116,9 +170,12 @@ export async function del(id, flags) {
|
|
|
116
170
|
requireArg(id, 'id', usage);
|
|
117
171
|
// Erases rather than closes. `resolve` is the reversible one; this is not,
|
|
118
172
|
// so it takes the same confirmation every destructive verb here takes.
|
|
119
|
-
await confirmDestructive(flags, `permanently delete feedback ${id} (resolve keeps
|
|
173
|
+
await confirmDestructive(flags, `permanently delete feedback ${id} — the report AND its screenshot (resolve keeps both)`, usage);
|
|
120
174
|
await sdkFeedback.deleteFeedback(config.api_key, orgId, id);
|
|
121
175
|
success(`Deleted ${id}`);
|
|
176
|
+
// The cascade is the point: somebody deleting a report because of what is IN
|
|
177
|
+
// it needs the picture gone too, and it used to be left behind.
|
|
178
|
+
info('The stored screenshot went with it.');
|
|
122
179
|
info('(Idempotent — an unknown id answers the same way, so this is not confirmation the item existed.)');
|
|
123
180
|
}
|
|
124
181
|
export async function widget(sub, arg, flags) {
|
package/dist/errors.js
CHANGED
|
@@ -26,6 +26,7 @@ export const ERROR_MESSAGES = {
|
|
|
26
26
|
invalid_org: 'Invalid organization.',
|
|
27
27
|
FORBIDDEN: 'You do not have permission to perform this action.',
|
|
28
28
|
RATE_LIMITED: 'Too many requests. Please wait a moment and try again.',
|
|
29
|
+
BODY_TOO_LARGE: 'The request is over 64 KB — usually an oversized feedback `trace`. Nothing was saved. This used to surface as INVALID_JSON, which sent people looking for a syntax error they did not have.',
|
|
29
30
|
INSUFFICIENT_BALANCE: 'Insufficient balance. Run: myapi billing topup <amount>',
|
|
30
31
|
INVALID_AMOUNT: 'Amount out of range. Maximum single top-up is $100. Run: myapi billing topup <amount>',
|
|
31
32
|
SERVICE_NOT_LAUNCHED: 'This service is disabled pre-launch. Track availability via: myapi status',
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: my-feedback-api
|
|
3
|
-
version: 1.
|
|
3
|
+
version: 1.5.1
|
|
4
4
|
description: >
|
|
5
5
|
Collect feedback from the people using what you built. A public widget key lets a page submit without a credential; you list, filter and resolve the results. Kind is chosen by the person reporting, not inferred from their wording.
|
|
6
6
|
triggers: [feedback, bug report, user feedback, feature request, widget, support, complaints, praise]
|
|
7
|
-
checksum: sha256-
|
|
7
|
+
checksum: sha256-b1aba273d6d32b4145dcee85fb59fec35f50b257fc41fb8da9f649a0b519e909
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# MyFeedbackAPI
|
|
@@ -42,11 +42,18 @@ theme baked in. This is the intended way in — you do not build a UI:
|
|
|
42
42
|
<script src="https://api.myapihq.com/feedback/in/<widget_key>/widget.js" async></script>
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
It renders a button
|
|
46
|
-
for
|
|
47
|
-
browsers within ~5 minutes with no redeploy of your site
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
It renders a button and shows itself only on the routes the widget is
|
|
46
|
+
configured for — set them with `widget update <id> --routes /app,/app/**`, and
|
|
47
|
+
the change reaches browsers within ~5 minutes with no redeploy of your site.
|
|
48
|
+
|
|
49
|
+
The person points at an element or drags a region, and it sends:
|
|
50
|
+
|
|
51
|
+
- `kind`, `body`, `page_url`, `route`, `viewport`
|
|
52
|
+
- `target_selector` and `target_region` `{x,y,w,h}` — the region is on **every**
|
|
53
|
+
report now: the element's bounding rectangle for a click, the dragged box for
|
|
54
|
+
a drag
|
|
55
|
+
- `target_context` — what the element *was*: its text, role, the heading above it
|
|
56
|
+
- `trace` — the last events before the report
|
|
50
57
|
|
|
51
58
|
A wrong key does not break the page: the response is `application/javascript`
|
|
52
59
|
carrying a JS comment that names the fix, so a typo cannot throw a syntax error
|
|
@@ -77,7 +84,9 @@ await fetch(`https://api.myapihq.com/feedback/in/${WIDGET_KEY}`, {
|
|
|
77
84
|
|
|
78
85
|
Errors: `WIDGET_NOT_FOUND` (a revoked key reads like an invented one, so keys
|
|
79
86
|
cannot be probed), `ORIGIN_NOT_ALLOWED`, `RATE_LIMITED`, `INVALID_KIND`,
|
|
80
|
-
`BODY_REQUIRED`, `BODY_TOO_LONG
|
|
87
|
+
`BODY_REQUIRED`, `BODY_TOO_LONG` (the text is over 8000 chars) and
|
|
88
|
+
`BODY_TOO_LARGE` (the whole request is over 64 KB, usually an oversized
|
|
89
|
+
`trace`). Nothing is saved in either case. The key only ever writes.
|
|
81
90
|
|
|
82
91
|
### Kind is a claim, not a guess
|
|
83
92
|
|
|
@@ -92,10 +101,19 @@ processed signal.
|
|
|
92
101
|
|
|
93
102
|
### Reading it back
|
|
94
103
|
|
|
95
|
-
Each item
|
|
96
|
-
`
|
|
97
|
-
|
|
98
|
-
`--
|
|
104
|
+
Each item carries what was pointed at, how they got there, and a picture.
|
|
105
|
+
`myapi feedback list` summarises that per report — the element's own text rather
|
|
106
|
+
than its CSS selector, a count of steps/errors/failed requests, and whether a
|
|
107
|
+
screenshot exists. `--trace` expands the event list; `--json` passes everything
|
|
108
|
+
through.
|
|
109
|
+
|
|
110
|
+
Two things not to misread: **`trace[].t` is milliseconds since page load**, not
|
|
111
|
+
a clock time, so it only means anything as a delta within one trace. And
|
|
112
|
+
**`target_context.filled` says an input had a value, never what it was** — the
|
|
113
|
+
widget does not collect it.
|
|
114
|
+
|
|
115
|
+
`delete` removes the screenshot along with the report, which is the case that
|
|
116
|
+
matters: somebody erasing a report because of what is in the picture.
|
|
99
117
|
|
|
100
118
|
`myapi feedback list` is newest first, filterable by `--kind` and `--status`
|
|
101
119
|
(`open` | `resolved`), paged with `--limit` / `--offset`. **`total` is the
|
|
@@ -112,7 +130,7 @@ that is deliberate, so ids cannot be probed across orgs.
|
|
|
112
130
|
| Command | What it does |
|
|
113
131
|
|---|---|
|
|
114
132
|
| `myapi feedback create "<text>" --kind <k>` | Record one item (`--page-url`, `--route` for context) |
|
|
115
|
-
| `myapi feedback list [--kind bug\|issue\|suggestion] [--status open\|resolved] [--limit N] [--offset N]` | List feedback, newest first |
|
|
133
|
+
| `myapi feedback list [--kind bug\|issue\|suggestion] [--status open\|resolved] [--limit N] [--offset N] [--trace]` | List feedback, newest first. Each report summarises what was pointed at, what happened before, and whether a screenshot exists; `--trace` expands the events |
|
|
116
134
|
| `myapi feedback resolve <id>` | Close an item, keeping it |
|
|
117
135
|
| `myapi feedback delete <id>` | Erase an item — for spam, or personal details typed into the box |
|
|
118
136
|
| `myapi feedback widget create <name> [--origins a.com,b.com]` | Mint a PUBLIC widget key for a site |
|
|
@@ -138,12 +156,13 @@ myapi feedback widget update <id> --routes /app,/app/**
|
|
|
138
156
|
myapi feedback list --status open
|
|
139
157
|
myapi feedback list --kind bug --limit 20
|
|
140
158
|
|
|
141
|
-
#
|
|
159
|
+
# 5. Record something yourself (support call, your own testing)
|
|
142
160
|
myapi feedback create "checkout 500s on the second attempt" --kind bug \
|
|
143
161
|
--route /checkout
|
|
144
162
|
|
|
145
|
-
#
|
|
163
|
+
# 6. Close it — or delete it, which also removes its screenshot
|
|
146
164
|
myapi feedback resolve <id>
|
|
165
|
+
myapi feedback delete <id> --yes
|
|
147
166
|
```
|
|
148
167
|
<!-- llm:end -->
|
|
149
168
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myapihq/cli",
|
|
3
3
|
"license": "Apache-2.0",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.15.1",
|
|
5
5
|
"description": "MyAPI command-line interface",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"lint:skills:strict": "node scripts/copy-skills.js && node scripts/lint-skills.js --strict"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@myapihq/sdk": "^2.
|
|
49
|
+
"@myapihq/sdk": "^2.15.1"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@types/node": "^25.6.0",
|