@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 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 and one open per page load (platform/docs/telemetry.md):
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 [docs/examples.md](../../docs/examples.md).
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
  ```
@@ -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:not(.recommendations-list__item--empty)",
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": { "selector": ".recommendations-list__text", "pattern": "[\"“”]([^\"“”]{1,100})[\"“”]" },
159
- "category": { "selector": ".recommendations-list__category-tag" }
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. Read only what the view needs. Settings and the Apps pages never show actions.
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) and the page's uncaught
1415
- errors and unhandled rejections: at most 10 per page load, each once, without query strings. The app's team sees them
1416
- with `hawk-app errors` and `hawk-app usage`, and Bridgeline in the portal (platform/docs/telemetry.md).
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
- `createApp({ reportErrors: false, reportOpens: false })` turns either off.
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 gallery app,
89
- [examples/ui-kit](../../examples/ui-kit), as a preview; run it with `npm run dev` there and open it through a host to
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.20261026155507",
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",
@@ -120,7 +120,8 @@
120
120
  "hawksearch:recommendations",
121
121
  "storage",
122
122
  "assistant",
123
- "hawkai:read"
123
+ "hawkai:read",
124
+ "comments"
124
125
  ]
125
126
  },
126
127
  "uniqueItems": true
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, and its opens, users and clients per day
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: 'Opens of the app and of its views, distinct users and clients per day on the dev platform (Bridgeline staff counted apart).',
345
- inputSchema: { days: z.number().int().min(1).max(400).optional().describe('Days back; default 30.'), dir: dirParam },
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, dir }) => result(await run(['usage', ...(days ? ['--days', String(days)] : [])], { dir })),
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();
@@ -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
- // and one open per page load), as the platform groups them. Read-only. `hawk-app errors --explain <id>` adds the
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
- /** `hawk-app usage [--days 30]`: opens and users per day and client. */
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
  }