@bridgeline-digital/hawk-app-cli 0.1.1-dev.20261026155507 → 0.1.1-dev.20261026194933
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/README.md +9 -2
- package/agents/AGENTS.md +10 -0
- package/docs/bridge/README.md +66 -8
- package/docs/ui/README.md +8 -3
- package/package.json +1 -1
- package/schema/hawk-app.schema.json +2 -1
- package/src/cli.mjs +7 -2
- package/src/manifest.mjs +17 -1
- package/src/mcp.mjs +10 -3
- package/src/platform.mjs +2 -0
- package/src/scope-check.mjs +1 -0
- package/src/telemetry.mjs +66 -2
package/README.md
CHANGED
|
@@ -306,14 +306,21 @@ HawkSearch). The full picture, with diagrams: docs/versions-and-aliases.md in th
|
|
|
306
306
|
|
|
307
307
|
## Errors and usage (`hawk-app errors`, `hawk-app usage`)
|
|
308
308
|
|
|
309
|
-
The bridge reports the app's page errors
|
|
309
|
+
The bridge reports the app's page errors, one open per page load, and the screens and actions used
|
|
310
|
+
(platform/docs/telemetry.md):
|
|
310
311
|
|
|
311
312
|
```bash
|
|
312
313
|
npx hawk-app errors # the last 7 days (--days 1-30): grouped, the most frequent first, with versions, clients and an [id]
|
|
313
314
|
npx hawk-app errors --explain 3fa1c2d9 # one error's brief, then Claude's likely cause and patch (--no-ai: the brief only)
|
|
314
315
|
npx hawk-app usage # the last 30 days (--days 1-400): opens, view opens, users and clients; staff apart
|
|
316
|
+
npx hawk-app usage --screens # screens and actions: the most used, then the least used, with the 3 newest versions
|
|
317
|
+
npx hawk-app usage --screens --version 4 # one version's counts
|
|
315
318
|
```
|
|
316
319
|
|
|
320
|
+
**What people use.** Screens are the app's routes (ids as `:id`), its views, and what it names with `app.usage.screen`
|
|
321
|
+
(the UI kit's `useScreen('Logs')`, for a tab); actions are clicks on `data-hawk-usage="Export CSV"` elements and
|
|
322
|
+
`app.usage.action`. Compare versions to see whether a change is used, and the least used to see what to simplify.
|
|
323
|
+
|
|
317
324
|
**Errors that come with a fix.** `--explain <id>` writes the error's **brief** and asks Claude for the cause:
|
|
318
325
|
|
|
319
326
|
- **The stack, mapped to your source:** each of the app's frames as `src/app/page.tsx:42:17`, with the code around it
|
|
@@ -739,7 +746,7 @@ attribute (`"data": "hawk-keyword"`), used as is when the page has it, with its
|
|
|
739
746
|
(the bridge README's **Actions**, with every target setting). `hawk-app validate` checks the targets (paths and
|
|
740
747
|
patterns are regular expressions, params the view declares); `hawk-app status` lists the actions. `hawk-app dev` has an
|
|
741
748
|
**Action** menu: pick an action, type a value, and the view opens in a dialog as in HawkSearch. For complete recipes
|
|
742
|
-
(actions, schedules, events, a client API endpoint and their tests), see [
|
|
749
|
+
(actions, schedules, events, a client API endpoint and their tests), see the [Recipes](../../docs/examples.md).
|
|
743
750
|
|
|
744
751
|
### Testing functions (`hawk-app test`)
|
|
745
752
|
|
package/agents/AGENTS.md
CHANGED
|
@@ -146,10 +146,19 @@ With the UI kit: `usePageContext(rows ? { page: '<the page>', dateRange, totals,
|
|
|
146
146
|
`app.assistant.setContext({ ... })` whenever the data or filters change. Plain names and numbers of what's on screen,
|
|
147
147
|
at most 50 KB; summarize long tables. `hawk-app validate` warns when an app never sets one.
|
|
148
148
|
|
|
149
|
+
**Comments** (scope `comments`): `<HawkComments target="keyword:…" label="…" />` from the UI kit on what people discuss
|
|
150
|
+
(a row, a widget, `page:summary`); targets are stable ids, never personal data. Declare `config.commentEmails`
|
|
151
|
+
(`{ "type": "emails" }`) so new comments are emailed (`hawk-app config set commentEmails …`, a person or a Slack
|
|
152
|
+
channel's email).
|
|
153
|
+
|
|
149
154
|
**Export as PDF** (only on pages worth keeping: a report, a summary): `usePdfExport({ fileName })` from the UI kit
|
|
150
155
|
(`false` while loading), or `app.setPdfExport({ fileName })` and `app.setPdfExport(null)` when the page leaves. The
|
|
151
156
|
host shows a PDF icon and downloads the whole page; mark buttons and filters `data-hawk-pdf-ignore`.
|
|
152
157
|
|
|
158
|
+
**Usage** is counted by the bridge: routes and views as screens. Name what they don't show: `useScreen('Logs')` from the
|
|
159
|
+
UI kit in each tab (or `app.usage.screen`), and mark the main buttons `data-hawk-usage="Export CSV"` (or
|
|
160
|
+
`app.usage.action`). Name the thing, never data (a keyword, a client).
|
|
161
|
+
|
|
153
162
|
**Actions** (`actions` in `hawk-app.json`) add a button to HawkSearch's own pages that opens a view in a dialog:
|
|
154
163
|
`{ "<name>": { "label": "Fix with …", "view": "fix", "targets": { "hawksearch": [{ "path": "<regex on the page path>",
|
|
155
164
|
"selector": "<CSS: each thing>", "anchor"?: "<CSS inside it>", "position"?: "after", "params": { "keyword": { "data"?,
|
|
@@ -195,6 +204,7 @@ npx hawk-app config list # config values (config set <key> <value> [--cl
|
|
|
195
204
|
npx hawk-app errors # errors in the app's pages (deployed versions), the most frequent first, with [id]s
|
|
196
205
|
npx hawk-app errors --explain <id> # one error mapped to the source, the failed calls, what changed; then a likely fix
|
|
197
206
|
npx hawk-app usage # opens, users and clients per day
|
|
207
|
+
npx hawk-app usage --screens # the screens and actions used, most and least, per version (--version <n>)
|
|
198
208
|
npx hawk-app upgrade # bring the app (and this file) up to the CLI's current template
|
|
199
209
|
npx hawk-app upgrade --channel latest # a dev app (bridge/CLI on the `dev` tag) to the released versions
|
|
200
210
|
```
|
package/docs/bridge/README.md
CHANGED
|
@@ -150,13 +150,15 @@ buttons appear in HawkSearch: nothing changes in HawkSearch for it.
|
|
|
150
150
|
"params": { "keyword": { "selector": "td.table__cell:first-child" } } // its text
|
|
151
151
|
},
|
|
152
152
|
{
|
|
153
|
+
// AI Advisor: only the insights that suggest a synonym or a redirect (HawkSearch marks each insight with
|
|
154
|
+
// data-hawk-action, data-hawk-category, data-hawk-keyword and data-hawk-insight-id).
|
|
153
155
|
"path": "/admin/recommendations/?$",
|
|
154
|
-
"selector": ".recommendations-list__item
|
|
156
|
+
"selector": ".recommendations-list__item[data-hawk-action=\"Synonym\"], .recommendations-list__item[data-hawk-action=\"Redirect\"]",
|
|
155
157
|
"anchor": ".recommendations-list__text",
|
|
156
158
|
"position": "after",
|
|
157
159
|
"params": {
|
|
158
|
-
"keyword": { "
|
|
159
|
-
"category": { "
|
|
160
|
+
"keyword": { "data": "hawk-keyword" }, // the insight's exact term
|
|
161
|
+
"category": { "data": "hawk-category" }
|
|
160
162
|
}
|
|
161
163
|
}
|
|
162
164
|
]
|
|
@@ -177,7 +179,8 @@ buttons appear in HawkSearch: nothing changes in HawkSearch for it.
|
|
|
177
179
|
- **Pick stable selectors:** ids, `data-*` attributes and element names over styling classes. HawkSearch doesn't know
|
|
178
180
|
about your selectors: when it changes its markup, your buttons stop appearing until you deploy new targets.
|
|
179
181
|
- **Approval:** `actions` is a setting that always needs an approver (never auto-approve): reviewers see each target and
|
|
180
|
-
what it reads from the page.
|
|
182
|
+
what it reads from the page. Before approval, Bridgeline staff already get the new actions on a draft app (the version
|
|
183
|
+
they open), so they can try the buttons in HawkSearch first; clients only ever get approved ones. Read only what the view needs. Settings and the Apps pages never show actions.
|
|
181
184
|
|
|
182
185
|
```tsx
|
|
183
186
|
// src/app/views/fix/page.tsx
|
|
@@ -1209,6 +1212,46 @@ await app.storage.files.delete('exports/prices.csv');
|
|
|
1209
1212
|
|
|
1210
1213
|
See the platform's docs/storage.md.
|
|
1211
1214
|
|
|
1215
|
+
## Comments: `comments`
|
|
1216
|
+
|
|
1217
|
+
Let the people using the app talk about what it shows, right where it is: a comment thread on a search term, a report
|
|
1218
|
+
row, a widget or the whole page. Everyone at the client who uses the app sees the comments, and so do Bridgeline staff.
|
|
1219
|
+
They're kept until deleted, and each new one is emailed to whoever you set.
|
|
1220
|
+
|
|
1221
|
+
```json
|
|
1222
|
+
{
|
|
1223
|
+
"scopes": ["comments"],
|
|
1224
|
+
"config": { "commentEmails": { "type": "emails", "label": "Who gets new comments" } }
|
|
1225
|
+
}
|
|
1226
|
+
```
|
|
1227
|
+
|
|
1228
|
+
```tsx
|
|
1229
|
+
// The UI kit: a badge with the count (one request for all the page's badges); a click opens the thread.
|
|
1230
|
+
<HawkComments target={`keyword:${row.keyword}`} label={row.keyword} />
|
|
1231
|
+
<HawkComments target="page:summary" label="This month's summary" />
|
|
1232
|
+
```
|
|
1233
|
+
|
|
1234
|
+
```ts
|
|
1235
|
+
// Or the bridge itself:
|
|
1236
|
+
const thread = await app.comments.list('keyword:rain-boots'); // [{ id, text, author, createdAt, mine, canDelete, … }], oldest first
|
|
1237
|
+
await app.comments.add('keyword:rain-boots', 'Catalog gap or synonym?', { label: 'rain boots' });
|
|
1238
|
+
await app.comments.edit('keyword:rain-boots', id, 'Synonym: we sell rain boots.'); // the author only
|
|
1239
|
+
await app.comments.delete('keyword:rain-boots', id); // the author, or Bridgeline staff
|
|
1240
|
+
const counts = await app.comments.counts(['keyword:rain-boots', 'keyword:wellies']); // { 'keyword:rain-boots': 2 }
|
|
1241
|
+
```
|
|
1242
|
+
|
|
1243
|
+
- **Targets** are yours: stable ids of 1-200 characters (letters, digits, `. _ - : / # @`), never personal data. The
|
|
1244
|
+
same target on two pages is the same thread. `label` names it in words for the drawer and the email.
|
|
1245
|
+
- **Email:** set `commentEmails` (declared as `emails` in `config`) to a person, a team list or a Slack channel's
|
|
1246
|
+
email address (Slack: the channel's Integrations › Send emails to this channel), for every client or one:
|
|
1247
|
+
`npx hawk-app config set commentEmails merch@agency.example [--client e3141]`. No deploy needed. Each new comment is
|
|
1248
|
+
sent there from the platform, with the app's name, the target and the client; replies go to the author. Without the
|
|
1249
|
+
key, nothing is sent. `hawk-app validate` warns about it.
|
|
1250
|
+
- **Authors** are shown by the email the host sends; Bridgeline staff are marked. Text only, up to 4,000 characters;
|
|
1251
|
+
30 new or edited comments a minute per app and client (429 `too_many_requests`).
|
|
1252
|
+
- Errors (`HawkApiError`): 400 `invalid_target` / `invalid_comment`, 403 `scope_missing` / `not_author`, 404
|
|
1253
|
+
`comment_not_found`. See the platform's docs/comments.md.
|
|
1254
|
+
|
|
1212
1255
|
## External Lambdas: `lambdas:invoke`
|
|
1213
1256
|
|
|
1214
1257
|
Call a Lambda written outside Hawk Apps (for example HawkAnalytics' client setup) from the app or a function. Declare
|
|
@@ -1411,9 +1454,10 @@ A `403` without one of these codes means the app lacks the scope for that call.
|
|
|
1411
1454
|
|
|
1412
1455
|
## Errors and usage
|
|
1413
1456
|
|
|
1414
|
-
`createApp` reports, on its own, one open per page load (the full app or the view showing)
|
|
1415
|
-
errors and unhandled rejections: at most 10 per page load, each once, without query
|
|
1416
|
-
with `hawk-app errors` and `hawk-app usage`, and Bridgeline in the portal
|
|
1457
|
+
`createApp` reports, on its own, one open per page load (the full app or the view showing), the screens and actions
|
|
1458
|
+
used, and the page's uncaught errors and unhandled rejections: at most 10 per page load, each once, without query
|
|
1459
|
+
strings. The app's team sees them with `hawk-app errors` and `hawk-app usage`, and Bridgeline in the portal
|
|
1460
|
+
(platform/docs/telemetry.md).
|
|
1417
1461
|
|
|
1418
1462
|
```ts
|
|
1419
1463
|
try {
|
|
@@ -1424,7 +1468,21 @@ try {
|
|
|
1424
1468
|
}
|
|
1425
1469
|
```
|
|
1426
1470
|
|
|
1427
|
-
|
|
1471
|
+
**What people use.** The bridge also counts the **screens** people open (each route of the full app, as `/orders/:id`
|
|
1472
|
+
with ids left out, or the view showing) and the **actions** they take: clicks on elements marked `data-hawk-usage`. Only
|
|
1473
|
+
these names are sent, batched every 15 s, never what users type or see. Count what the routes don't show yourself:
|
|
1474
|
+
|
|
1475
|
+
```tsx
|
|
1476
|
+
<Button data-hawk-usage="Export CSV" onClick={exportCsv}>Export</Button> // counted on each click
|
|
1477
|
+
|
|
1478
|
+
app.usage.screen('Logs'); // a tab or step within one route (the UI kit: useScreen('Logs') in the tab)
|
|
1479
|
+
app.usage.action('Bulk fix'); // an action that isn't a click on one element
|
|
1480
|
+
```
|
|
1481
|
+
|
|
1482
|
+
Names are letters, digits and simple punctuation, up to 80 characters: name the thing (`Export CSV`), not the data
|
|
1483
|
+
(never a keyword or a client). `hawk-app usage --screens` lists the most and least used, per version.
|
|
1484
|
+
|
|
1485
|
+
`createApp({ reportErrors: false, reportOpens: false, reportUsage: false })` turns any of them off.
|
|
1428
1486
|
|
|
1429
1487
|
## Talk to the host
|
|
1430
1488
|
|
package/docs/ui/README.md
CHANGED
|
@@ -42,9 +42,15 @@ export function Page() {
|
|
|
42
42
|
then renders its children in the theme. It follows the host when its theme changes.
|
|
43
43
|
- **`useHawk()`** returns `{ app, context }`, always connected (the provider renders children only then).
|
|
44
44
|
`useHawkAppState()` is the hook underneath, for an app that wants to render the states itself.
|
|
45
|
+
- **`<HawkComments target label?>`** (the `comments` scope) is a comment badge with its count; a click opens the
|
|
46
|
+
thread in a drawer, where everyone at the client reads and adds comments (authors edit and delete their own). The
|
|
47
|
+
page's badges share one count request. See the bridge guide's "Comments".
|
|
45
48
|
- **`usePdfExport(options | false)`** lets the user export the page as a PDF while it's mounted: the host shows a PDF
|
|
46
49
|
icon, and a click downloads the whole page. `usePdfExport({ fileName: 'Zero results' })`, or `false` while loading.
|
|
47
50
|
Leave out buttons and filters with `data-hawk-pdf-ignore`. See the bridge guide's "Export a page as a PDF".
|
|
51
|
+
- **`useScreen(name | null)`** counts a screen the routes don't show, such as a tab, each time it's shown:
|
|
52
|
+
`useScreen('Logs')` in the tab's component. Routes and views are counted by the bridge, and so are clicks on
|
|
53
|
+
`data-hawk-usage="Export CSV"` elements. See `hawk-app usage --screens` and the bridge guide's "Errors and usage".
|
|
48
54
|
- **`usePageContext(object | null)`** says what the page shows, for the host's **Explain this page** button (and its
|
|
49
55
|
assistant): `usePageContext(rows ? { page: 'Zero-result searches', dateRange, top10 } : null)`. Plain names and
|
|
50
56
|
numbers, at most 50 KB; sent shortly after it changes, cleared when the component unmounts. See the bridge guide's
|
|
@@ -85,9 +91,8 @@ const f = useFormat(); // in the host's locale
|
|
|
85
91
|
|
|
86
92
|
The host shows the frame over the whole page while a dialog or drawer is open, so they're always fully visible.
|
|
87
93
|
|
|
88
|
-
Every component, in light and dark, is in the [UI kit gallery](GALLERY.md) on the developer site (the
|
|
89
|
-
|
|
90
|
-
see it in a real one). `<HawkAppProvider preview={{ mode: 'dark' }}>` renders without a host, with sample context
|
|
94
|
+
Every component, in light and dark, is in the [UI kit gallery](GALLERY.md) on the developer site (the repository's
|
|
95
|
+
gallery app, `examples/ui-kit`, rendered as a preview). `<HawkAppProvider preview={{ mode: 'dark' }}>` renders without a host, with sample context
|
|
91
96
|
(`previewContext()`), for galleries, docs and screenshots. New apps get the kit from the CLI's `next-mui` template
|
|
92
97
|
(`hawk-app create my-app --template next-mui`); an existing next-mui app gets it with `hawk-app upgrade`.
|
|
93
98
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bridgeline-digital/hawk-app-cli",
|
|
3
|
-
"version": "0.1.1-dev.
|
|
3
|
+
"version": "0.1.1-dev.20261026194933",
|
|
4
4
|
"description": "For Bridgeline developers: create, deploy and register Hawk Apps (Services-built tools embedded in HawkSearch). Requires a Hawk Apps publisher key.",
|
|
5
5
|
"author": "Bridgeline Digital",
|
|
6
6
|
"license": "UNLICENSED",
|
package/src/cli.mjs
CHANGED
|
@@ -65,7 +65,7 @@ Data and functions
|
|
|
65
65
|
secrets, logs Function secrets and logs
|
|
66
66
|
|
|
67
67
|
Monitor
|
|
68
|
-
errors, usage The app's page errors
|
|
68
|
+
errors, usage The app's page errors; its opens, users, clients, screens and actions
|
|
69
69
|
|
|
70
70
|
Account
|
|
71
71
|
login, whoami, logout A publisher key (for CI), and which key and platform apply here
|
|
@@ -196,6 +196,11 @@ Monitor:
|
|
|
196
196
|
cause and patch (--no-ai: the brief only)
|
|
197
197
|
hawk-app usage [--days 30] Opens of the app and its views, users and clients per day (up to
|
|
198
198
|
400 days; Bridgeline staff counted apart)
|
|
199
|
+
hawk-app usage --screens [--days 30] [--version <n>]
|
|
200
|
+
The screens (routes, views, app.usage.screen) and actions
|
|
201
|
+
([data-hawk-usage], app.usage.action) people use: the most used
|
|
202
|
+
first, the least used last, the newest versions side by side
|
|
203
|
+
(--version: one version's counts)
|
|
199
204
|
|
|
200
205
|
Custom data (app queries, scope data:read):
|
|
201
206
|
hawk-app data tables [--app <slug>] List the tables queries can use (--app: is the app
|
|
@@ -852,7 +857,7 @@ export function parse(argv) {
|
|
|
852
857
|
flags.param = [...(flags.param ?? []), inline ?? argv[++i]];
|
|
853
858
|
continue;
|
|
854
859
|
}
|
|
855
|
-
const takesValue = ['manifest', 'platform', 'slug', 'name', 'template', 'client', 'app', 'since', 'value-from-env', 'engine', 'url', 'port', 'env', 'publisher', 'pick', 'channel', 'days', 'alias', 'explain'].includes(name);
|
|
860
|
+
const takesValue = ['manifest', 'platform', 'slug', 'name', 'template', 'client', 'app', 'since', 'value-from-env', 'engine', 'url', 'port', 'env', 'publisher', 'pick', 'channel', 'days', 'alias', 'explain', 'version'].includes(name);
|
|
856
861
|
flags[name] = takesValue ? inline ?? argv[++i] : true;
|
|
857
862
|
} else if (command === undefined) {
|
|
858
863
|
command = arg;
|
package/src/manifest.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { validateStore } from './store-listing.mjs';
|
|
|
6
6
|
export const MANIFEST_FILE = 'hawk-app.json';
|
|
7
7
|
const SLUG = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
8
8
|
const VISIBILITIES = ['allowList', 'all', 'internal', 'store'];
|
|
9
|
-
const SCOPES = ['reports:read', 'hawksearch:read', 'hawksearch:write', 'data:read', 'functions:invoke', 'email:send', 'hawksearch:sysparams:write', 'hawksearch:search', 'ai:invoke', 'lambdas:invoke', 'hawksearch:features:write', 'hawksearch:index', 'hawksearch:index:items', 'hawksearch:index:manage', 'hawksearch:recommendations', 'storage', 'assistant', 'hawkai:read'];
|
|
9
|
+
const SCOPES = ['reports:read', 'hawksearch:read', 'hawksearch:write', 'data:read', 'functions:invoke', 'email:send', 'hawksearch:sysparams:write', 'hawksearch:search', 'ai:invoke', 'lambdas:invoke', 'hawksearch:features:write', 'hawksearch:index', 'hawksearch:index:items', 'hawksearch:index:manage', 'hawksearch:recommendations', 'storage', 'assistant', 'hawkai:read', 'comments'];
|
|
10
10
|
export const SYSPARAMS_SCOPE = 'hawksearch:sysparams:write';
|
|
11
11
|
/** The templates `hawk-app create` offers (the keys of create.mjs's TEMPLATES): `"template"` in hawk-app.json. */
|
|
12
12
|
export const TEMPLATE_NAMES = ['react', 'preact', 'next', 'next-mui', 'vanilla'];
|
|
@@ -400,6 +400,7 @@ export function validateManifest(manifest) {
|
|
|
400
400
|
errors.push(...store.errors);
|
|
401
401
|
warnings.push(...store.warnings);
|
|
402
402
|
warnings.push(...trialWarnings(m));
|
|
403
|
+
warnings.push(...commentWarnings(m));
|
|
403
404
|
|
|
404
405
|
const unknownScopes = (m.scopes ?? []).filter((s) => !SCOPES.includes(s));
|
|
405
406
|
if (unknownScopes.length) warnings.push(`These scopes are not available and will not be granted: ${unknownScopes.join(', ')}.`);
|
|
@@ -409,6 +410,21 @@ export function validateManifest(manifest) {
|
|
|
409
410
|
return { errors, warnings };
|
|
410
411
|
}
|
|
411
412
|
|
|
413
|
+
/** The config key the platform emails new comments to (comments scope): it must be an `emails` key. */
|
|
414
|
+
export const COMMENT_EMAILS_KEY = 'commentEmails';
|
|
415
|
+
|
|
416
|
+
/** Comments' email: `commentEmails` declared as another type gets nothing; the comments scope without it emails no one. */
|
|
417
|
+
function commentWarnings(m) {
|
|
418
|
+
const declared = m.config && typeof m.config === 'object' ? m.config[COMMENT_EMAILS_KEY] : undefined;
|
|
419
|
+
if (declared && declared.type !== 'emails') {
|
|
420
|
+
return [`config.${COMMENT_EMAILS_KEY} must be of type "emails" for new comments to be emailed to it.`];
|
|
421
|
+
}
|
|
422
|
+
if ((m.scopes ?? []).includes('comments') && !declared) {
|
|
423
|
+
return [`The app has comments but no config.${COMMENT_EMAILS_KEY} ({ "type": "emails" }): new comments are emailed to no one. Declare it, then \`hawk-app config set ${COMMENT_EMAILS_KEY} team@example.com\`.`];
|
|
424
|
+
}
|
|
425
|
+
return [];
|
|
426
|
+
}
|
|
427
|
+
|
|
412
428
|
/**
|
|
413
429
|
* "lambdas": Lambdas outside Hawk Apps the app calls (scope lambdas:invoke). A list of up to 10 unique Lambda function
|
|
414
430
|
* names; the list needs the scope and the scope needs a non-empty list. Whether a platform allows a name is only known
|
package/src/mcp.mjs
CHANGED
|
@@ -341,11 +341,18 @@ export function createMcpServer({
|
|
|
341
341
|
'usage',
|
|
342
342
|
{
|
|
343
343
|
title: 'App usage on dev',
|
|
344
|
-
description:
|
|
345
|
-
|
|
344
|
+
description:
|
|
345
|
+
'Opens of the app and of its views, distinct users and clients per day on the dev platform (Bridgeline staff counted apart). With screens: true, the screens and actions people use instead, the most and least used, per version: what to improve or remove.',
|
|
346
|
+
inputSchema: {
|
|
347
|
+
days: z.number().int().min(1).max(400).optional().describe('Days back; default 30.'),
|
|
348
|
+
screens: z.boolean().optional().describe('The screens (routes, views, named tabs) and actions (marked clicks) used, instead of opens.'),
|
|
349
|
+
version: z.number().int().min(1).optional().describe('With screens: one version\'s counts only.'),
|
|
350
|
+
dir: dirParam,
|
|
351
|
+
},
|
|
346
352
|
annotations: { ...readOnly, openWorldHint: true },
|
|
347
353
|
},
|
|
348
|
-
async ({ days,
|
|
354
|
+
async ({ days, screens, version, dir }) =>
|
|
355
|
+
result(await run(['usage', ...(screens ? ['--screens'] : []), ...(screens && version ? ['--version', String(version)] : []), ...(days ? ['--days', String(days)] : [])], { dir })),
|
|
349
356
|
);
|
|
350
357
|
|
|
351
358
|
server.registerTool(
|
package/src/platform.mjs
CHANGED
|
@@ -142,6 +142,8 @@ export function platformClient({ url = process.env.HAWK_APPS_PLATFORM_URL, key =
|
|
|
142
142
|
},
|
|
143
143
|
/** The app's opens over `days`: { opens, viewOpens, staffOpens, users, clients, perDay, perClient }. */
|
|
144
144
|
appUsage: (slug, days) => call('GET', `/publish/apps/${encodeURIComponent(slug)}/usage?days=${days}`),
|
|
145
|
+
/** Screens and actions used: { days, versions, screens: [{ name, count, staffCount, clients, versions: [{ version, count }] }], actions }. */
|
|
146
|
+
appScreens: (slug, days) => call('GET', `/publish/apps/${encodeURIComponent(slug)}/usage/screens?days=${days}`),
|
|
145
147
|
/** One page of a function's logs: { events: [{ timestamp, message }], nextToken }. */
|
|
146
148
|
functionLogs: (slug, name, { since, nextToken } = {}) => {
|
|
147
149
|
const query = new URLSearchParams();
|
package/src/scope-check.mjs
CHANGED
|
@@ -28,6 +28,7 @@ const RULES = [
|
|
|
28
28
|
{ scope: 'hawksearch:index:manage', pattern: /\.hawksearch\.index\.(create|setCurrent|deleteIndex|rebuildAll)\s*\(/ },
|
|
29
29
|
{ scope: 'hawksearch:recommendations', pattern: /\.hawksearch\.recommendations\s*\(/ },
|
|
30
30
|
{ scope: 'storage', pattern: /\.storage\.(get|put|delete|list|batch|usage)\s*\(|\.storage\.files\.\w+\s*\(/ },
|
|
31
|
+
{ scope: 'comments', pattern: /\.comments\.(list|add|edit|delete|counts)\s*\(|<HawkComments\b/ },
|
|
31
32
|
{ scope: 'lambdas:invoke', pattern: /\.lambdas\.invoke\s*\(/ },
|
|
32
33
|
{ scope: 'ai:invoke', pattern: /\.ai\.(generate|stream|info)\s*\(/ },
|
|
33
34
|
{ scope: 'assistant', pattern: /\.assistant\.(ask|setContext)\s*\(/ },
|
package/src/telemetry.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// `hawk-app errors` and `hawk-app usage`: what the bridge reports from the app's pages (uncaught errors and rejections,
|
|
2
|
-
//
|
|
2
|
+
// one open per page load, the screens and actions used), as the platform groups them. Read-only. `hawk-app errors --explain <id>` adds the
|
|
3
3
|
// error's brief (error-brief.mjs) and Claude's likely cause and patch.
|
|
4
4
|
import { errorBrief, errorId, findGroup } from './error-brief.mjs';
|
|
5
5
|
|
|
@@ -65,8 +65,13 @@ async function explainCommand({ client, manifest, flags, log, write }) {
|
|
|
65
65
|
return 0;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* `hawk-app usage [--days 30]`: opens and users per day and client.
|
|
70
|
+
* `hawk-app usage --screens [--days 30] [--version <n>]`: the screens and actions people use, the most used first and
|
|
71
|
+
* the least used last, with the newest versions side by side (or only one version's).
|
|
72
|
+
*/
|
|
69
73
|
export async function usageCommand({ client, manifest, flags = {}, log = console.log }) {
|
|
74
|
+
if (flags.screens) return screensCommand({ client, manifest, flags, log });
|
|
70
75
|
const days = parseDays(flags.days, 30, 400);
|
|
71
76
|
const usage = await client.appUsage(manifest.slug, days);
|
|
72
77
|
log(`${manifest.slug}, the last ${plural(days, 'day')}:`);
|
|
@@ -80,5 +85,64 @@ export async function usageCommand({ client, manifest, flags = {}, log = console
|
|
|
80
85
|
}
|
|
81
86
|
if (usage.perClient.length > 20) log(` … and ${usage.perClient.length - 20} more`);
|
|
82
87
|
}
|
|
88
|
+
log('');
|
|
89
|
+
log('Which screens and actions: hawk-app usage --screens');
|
|
90
|
+
return 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const MOST = 15;
|
|
94
|
+
const LEAST = 5;
|
|
95
|
+
const VERSION_COLUMNS = 3;
|
|
96
|
+
|
|
97
|
+
async function screensCommand({ client, manifest, flags, log }) {
|
|
98
|
+
const days = parseDays(flags.days, 30, 400);
|
|
99
|
+
let only = null;
|
|
100
|
+
if (flags.version !== undefined) {
|
|
101
|
+
only = Number(String(flags.version).replace(/^v/i, ''));
|
|
102
|
+
if (!Number.isInteger(only) || only < 1) throw new Error('--version must be a version number, e.g. --version 4.');
|
|
103
|
+
}
|
|
104
|
+
const usage = await client.appScreens(manifest.slug, days);
|
|
105
|
+
const countIn = (item, version) => item.versions.find((v) => v.version === version)?.count ?? 0;
|
|
106
|
+
// One version: its counts only (staff and clients are over all versions, so they're left out).
|
|
107
|
+
const pick = (items) =>
|
|
108
|
+
only === null
|
|
109
|
+
? items
|
|
110
|
+
: items
|
|
111
|
+
.map((item) => ({ ...item, count: countIn(item, only) }))
|
|
112
|
+
.filter((item) => item.count > 0)
|
|
113
|
+
.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
|
|
114
|
+
const screens = pick(usage.screens);
|
|
115
|
+
const actions = pick(usage.actions);
|
|
116
|
+
const columns = only === null ? usage.versions.slice(0, VERSION_COLUMNS) : [];
|
|
117
|
+
|
|
118
|
+
log(`${manifest.slug}, the last ${plural(days, 'day')}${only === null ? '' : `, v${only}`}:`);
|
|
119
|
+
if (!screens.length && !actions.length) {
|
|
120
|
+
log(only !== null && usage.versions.length ? ` Nothing counted in v${only} (versions used: ${usage.versions.map((v) => `v${v}`).join(', ')}).` : ' Nothing counted yet.');
|
|
121
|
+
log(' Screens are counted by the bridge (routes, views, app.usage.screen); actions are clicks on [data-hawk-usage] or app.usage.action.');
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
const table = (title, items) => {
|
|
125
|
+
if (!items.length) return;
|
|
126
|
+
const width = Math.min(40, Math.max(title.length, ...items.map((i) => i.name.length)));
|
|
127
|
+
const header = only === null ? ['Uses', 'Staff', 'Clients', ...columns.map((v) => `v${v}`)] : ['Uses'];
|
|
128
|
+
log('');
|
|
129
|
+
log(` ${title.padEnd(width)} ${header.map((h) => h.padStart(7)).join(' ')}`);
|
|
130
|
+
const row = (i) => {
|
|
131
|
+
const name = i.name.length > width ? `${i.name.slice(0, width - 1)}…` : i.name;
|
|
132
|
+
const cells = only === null ? [i.count, i.staffCount, i.clients, ...columns.map((v) => countIn(i, v))] : [i.count];
|
|
133
|
+
log(` ${name.padEnd(width)} ${cells.map((c) => String(c).padStart(7)).join(' ')}`);
|
|
134
|
+
};
|
|
135
|
+
if (items.length <= MOST + LEAST) {
|
|
136
|
+
items.forEach(row);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
items.slice(0, MOST).forEach(row);
|
|
140
|
+
log(` ${'…'.padEnd(width)} ${items.length - MOST - LEAST} more, then the least used:`);
|
|
141
|
+
items.slice(-LEAST).forEach(row);
|
|
142
|
+
};
|
|
143
|
+
table('Screens', screens);
|
|
144
|
+
table('Actions', actions);
|
|
145
|
+
log('');
|
|
146
|
+
log(`Uses are by your clients' users; Bridgeline staff are counted apart.${columns.length < usage.versions.length && only === null ? ' Older versions: --version <n>.' : ''}`);
|
|
83
147
|
return 0;
|
|
84
148
|
}
|