@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.
@@ -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, printTable, printJson, banner } from '../output.js';
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
- // The table cannot carry a signed URL, a selector and a navigation trace, so
60
- // it flags what exists and --json carries it. Saying nothing about a
61
- // screenshot would leave a picture nobody knows to look for.
62
- printTable((page.items ?? []).map(i => ({
63
- id: i.id,
64
- kind: i.kind,
65
- status: i.status,
66
- body: i.body.length > 60 ? `${i.body.slice(0, 57)}…` : i.body,
67
- route: i.route ?? i.page_url ?? '',
68
- // A failed signature still means a picture is there — distinguish that
69
- // from having none, rather than showing both as blank.
70
- shot: i.screenshot_url ? 'yes' : (i.screenshot_asset_id ? 'unsigned' : ''),
71
- created: i.created_at ? formatDate(i.created_at) : '',
72
- })), { flags, empty: 'No feedback yet. Put a widget on a page: myapi feedback widget create <name>' });
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 && !flags.json) {
75
- info(`${withShots} item${withShots === 1 ? ' has' : 's have'} a screenshot — \`--json\` carries the link (signed, ~1h).`);
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 it; this erases it)`, usage);
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.4.0
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-b8a6dbed6fa4aa393e538225ae611f5f5d8c4fbb1ffea7922e4a0286d984eca4
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, shows itself only on the routes the widget is configured
46
- for (set them with `widget update <id> --routes /app,/app/**`; changes reach
47
- browsers within ~5 minutes with no redeploy of your site), and lets the person point at an element or drag a region. It sends `kind`,
48
- `body`, `page_url`, `route`, `viewport`, plus **either** `target_selector` (they
49
- clicked something) **or** `target_region` `{x,y,w,h}` (they dragged a box).
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`. The key only ever writes.
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 also carries what was pointed at (`target_selector`, `target_region`,
96
- `target_context`), how they got there (`trace`), the `viewport`, and
97
- `screenshot_url` when there is a picture. The table flags which items have one;
98
- `--json` carries the link.
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
- # 3. Record something yourself (support call, your own testing)
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
- # 4. Close it
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.14.0",
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.14.0"
49
+ "@myapihq/sdk": "^2.15.1"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@types/node": "^25.6.0",