@bridgeline-digital/hawk-app-cli 0.1.1-dev.20261021024847 → 0.1.1-dev.20261026155507

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.
Files changed (46) hide show
  1. package/README.md +188 -24
  2. package/agents/AGENTS.md +32 -8
  3. package/claude-plugin/.claude-plugin/marketplace.json +14 -0
  4. package/claude-plugin/hawk-apps/.claude-plugin/plugin.json +8 -0
  5. package/claude-plugin/hawk-apps/.mcp.json +8 -0
  6. package/claude-plugin/hawk-apps/scripts/mcp.mjs +35 -0
  7. package/claude-plugin/hawk-apps/skills/add-function/SKILL.md +46 -0
  8. package/claude-plugin/hawk-apps/skills/add-query/SKILL.md +31 -0
  9. package/claude-plugin/hawk-apps/skills/new-screen/SKILL.md +36 -0
  10. package/claude-plugin/hawk-apps/skills/prepare-review/SKILL.md +36 -0
  11. package/docs/bridge/README.md +380 -24
  12. package/docs/showcase/hawk-app.json +8 -3
  13. package/docs/showcase/src/App.tsx +255 -1
  14. package/docs/ui/README.md +7 -0
  15. package/package.json +12 -1
  16. package/schema/hawk-app.schema.json +226 -2
  17. package/src/agents.mjs +38 -7
  18. package/src/claude-plugin.mjs +67 -0
  19. package/src/cli.mjs +56 -7
  20. package/src/deploy.mjs +16 -8
  21. package/src/dev.mjs +177 -4
  22. package/src/doctor.mjs +17 -3
  23. package/src/error-brief.mjs +128 -0
  24. package/src/function-test-runner.mjs +16 -0
  25. package/src/function-test.d.ts +51 -0
  26. package/src/function-test.mjs +303 -0
  27. package/src/functions.mjs +35 -21
  28. package/src/manifest.mjs +240 -1
  29. package/src/mcp-hawksearch.mjs +14 -4
  30. package/src/mcp.mjs +16 -0
  31. package/src/platform.mjs +64 -0
  32. package/src/project-checks.mjs +19 -5
  33. package/src/scope-check.mjs +9 -2
  34. package/src/sourcemaps.mjs +168 -0
  35. package/src/telemetry.mjs +38 -4
  36. package/src/template-history.json +8 -4
  37. package/src/test-command.mjs +88 -0
  38. package/templates/next/next.config.ts +3 -0
  39. package/templates/next/src/app/page.tsx +6 -0
  40. package/templates/next-mui/next.config.ts +3 -0
  41. package/templates/next-mui/src/app/page.tsx +3 -1
  42. package/templates/preact/src/App.tsx +6 -0
  43. package/templates/preact/vite.config.ts +3 -0
  44. package/templates/react/src/App.tsx +6 -0
  45. package/templates/react/vite.config.ts +3 -0
  46. package/templates/vanilla/public/app.js +2 -0
package/README.md CHANGED
@@ -68,8 +68,9 @@ separately, see [Clients](#clients)):
68
68
  // internal: Bridgeline users only; store: the app's clients, and every other
69
69
  // client can request it from the store (needs "store", below) (clients: see "Clients")
70
70
  "icon": "icon.svg", // the app's icon: an .svg (drawn in the host's color) or a square .png, next to this file
71
- "scopes": ["reports:read"] // reports:read = analytics; hawksearch:read = dashboard API (read);
72
- // hawksearch:write = create/update/delete synonyms, redirects, banners, …
71
+ "scopes": ["reports:read"] // reports:read = analytics; hawksearch:read = dashboard API (read) and preview tools;
72
+ // hawksearch:write = create/update/delete synonyms, redirects, banners, …,
73
+ // and pin items (hawksearch.pinning)
73
74
  // through the dashboard API (see the bridge's "Writing to HawkSearch");
74
75
  // data:read = your own SQL queries in queries/ (see "Custom data");
75
76
  // functions:invoke = server-side functions (see "Functions");
@@ -79,6 +80,8 @@ separately, see [Clients](#clients)):
79
80
  // (with "systemParameters", see "HawkSearch system parameters");
80
81
  // hawksearch:search = HawkSearch search and autocomplete, live or preview;
81
82
  // hawksearch:recommendations = the engine's recommendation widgets;
83
+ // hawkai:read = HawkAI agents and smart responses: list, run a smart
84
+ // response, open an agent in HawkAI's assistant widget;
82
85
  // hawksearch:index = rebuild the engine's index (where HawkSearch allows it)
83
86
  // and read its indexes; hawksearch:index:items = add, update, delete items;
84
87
  // hawksearch:index:manage = create, switch and delete indexes, hierarchy, mappings;
@@ -174,8 +177,8 @@ npx hawk-app dev --client e3141 --engine 3141
174
177
 
175
178
  The first time, it asks for part of a client's name and lets you pick the client, then saves it in `.env.local` as
176
179
  `HAWK_APPS_DEV_CLIENT` (`--client <id>` uses another). A new app is registered on the platform the first time (not
177
- released: only Bridgeline staff see it). Queries and functions always run from the **deployed** version, even here:
178
- deploy after changing them.
180
+ released: only Bridgeline staff see it). Queries and functions called from the app run from the **deployed** version,
181
+ even here: deploy after changing them (or test them locally with `hawk-app test`).
179
182
 
180
183
  Starts your app (`npm run dev`) and a local stand-in for HawkSearch at `http://localhost:3200`, which embeds the app
181
184
  the way HawkSearch does (the bridge's own `<hawk-app-frame>`). The app gets **real tokens and real data**: the CLI asks
@@ -193,7 +196,19 @@ CORS setup, on dev or prod; so does the AI stream (`app.ai.stream`), which the p
193
196
  `https://dev.hawksearch.net`; `HAWKSEARCH_GROUP_KEY=true` for a group key): the platform seals it into the token. It is
194
197
  never written anywhere else.
195
198
  - Queries and functions run against your **current deployed version** (deploy first after changing them); the UI is your
196
- local code. Every dev token is audit-logged with your publisher key.
199
+ local code. Every dev token is audit-logged with your publisher key. To run functions from your local code, use
200
+ `hawk-app test` ([Testing functions](#testing-functions-hawk-app-test)).
201
+ - **Actions:** when `hawk-app.json` has `actions`, the toolbar's **Action** menu opens an action's view in a dialog as
202
+ HawkSearch will (type the value its targets would read from the page); `app.action.done()` closes it.
203
+ - **Export as PDF:** the toolbar has HawkSearch's PDF icon. It shows while the page allows it (`app.setPdfExport` /
204
+ `usePdfExport`; the events list logs it), and a click downloads the page as a PDF.
205
+ - **Explain this page:** the toolbar has HawkSearch's **Explain this page** button. It shows once the page says what
206
+ it shows (`app.assistant.setContext` / `usePageContext`; the events list logs each one), and explains it with the
207
+ platform's AI for the client.
208
+ - **Client API:** `http://localhost:3200/v1/clients/<id>/apps/<slug>/api/<name>` (with `X-HawkSearch-ApiKey`) runs the
209
+ endpoint's function from your **local** `functions/` (bundled on each call), with a real token for the client whose
210
+ HawkSearch connection is the key you send. The platform's own checks (the key's engine, API access, the limit) are
211
+ skipped; secrets and config are empty.
197
212
 
198
213
  ## Deploy
199
214
 
@@ -215,7 +230,7 @@ npx hawk-app deploy --dry-run # what hawk-app.json would change, without a v
215
230
  | Command | What it does |
216
231
  |---|---|
217
232
  | `init` | Creates `hawk-app.json` (never overwrites an existing one) and adds [AGENTS.md](#ai-coding-assistants-agentsmd). |
218
- | `validate [--build [<folder>]]` | Checks `hawk-app.json` locally, `queries/` and `functions/` if the app has them, and the project: the bridge is used and current, and there's a build script (or `--no-build` deploys). `--build` also builds the app with a stand-in base path (`/<slug>/v0`) and fails on absolute URLs outside it; the folder comes from the `app:deploy` script unless given. |
233
+ | `validate [--build [<folder>]]` | Checks `hawk-app.json` locally, `queries/` and `functions/` if the app has them, and the project: the bridge is used and current, the app says what its page shows ("Explain this page"), and there's a build script (or `--no-build` deploys). `--build` also builds the app with a stand-in base path (`/<slug>/v0`) and fails on absolute URLs outside it; the folder comes from the `app:deploy` script unless given. |
219
234
  | `deploy [<folder>] [--no-build] [--no-audit] [--alias <name>] [--dry-run]` | Sends hawk-app.json first (its scopes and settings go with every deploy), then reserves the next version number, runs `npm run build` with `HAWK_APP_BASE_PATH=/<slug>/v<n>` (when package.json has a `build` script, unless `--no-build`), then uploads every file in the build folder (the template's, `dist`, `out` or `public`, unless given) to `/<slug>/v<n>/` on the apps CDN, plus `queries/` and the bundled `functions` (if any) to the platform's private storage, and deploys the functions. Runs `npm audit` of the production dependencies first, asks before deploying with high or critical findings, and reports the counts to reviewers (see [Dependency check](#dependency-check); `--no-audit` skips it). Warns when the HTML loads absolute URLs outside `/<slug>/v<n>/`, which wouldn't load, and when the code calls a bridge feature whose scope `hawk-app.json` lacks (e.g. `app.ai.generate` without `ai:invoke`; `validate` and `doctor` check it too). Says whether the version is a draft (staff only until it's approved) or auto-approved, and when it cleared an approval request. `--alias staging` points the alias at the new version (see [Versions and aliases](#versions-and-aliases)). `--dry-run` only shows what hawk-app.json would change. |
220
235
  | `register` | Sends `hawk-app.json` without a new version (for scripts; `deploy` and `dev` already send it). It never releases anything and never changes the app's clients. On a released app, changes wait for review. |
221
236
  | `versions` | Every version with its status (draft, waiting, approved, rejected) and the aliases on it. |
@@ -294,10 +309,30 @@ HawkSearch). The full picture, with diagrams: docs/versions-and-aliases.md in th
294
309
  The bridge reports the app's page errors and one open per page load (platform/docs/telemetry.md):
295
310
 
296
311
  ```bash
297
- npx hawk-app errors # the last 7 days (--days 1-30): grouped, the most frequent first, with versions and clients
312
+ npx hawk-app errors # the last 7 days (--days 1-30): grouped, the most frequent first, with versions, clients and an [id]
313
+ npx hawk-app errors --explain 3fa1c2d9 # one error's brief, then Claude's likely cause and patch (--no-ai: the brief only)
298
314
  npx hawk-app usage # the last 30 days (--days 1-400): opens, view opens, users and clients; staff apart
299
315
  ```
300
316
 
317
+ **Errors that come with a fix.** `--explain <id>` writes the error's **brief** and asks Claude for the cause:
318
+
319
+ - **The stack, mapped to your source:** each of the app's frames as `src/app/page.tsx:42:17`, with the code around it
320
+ and the line that threw marked `>`. The maps are the version's own, so the code is what ran, not what's in your
321
+ checkout now. Library frames are listed on one line each.
322
+ - **The platform calls that failed** in the same versions and days (route, status, code, detail), often the reason
323
+ (e.g. a report that answered 502 left `rows` undefined).
324
+ - **What changed since the last version without it:** a diff of the files in the stack between that version and the
325
+ one that threw.
326
+ - **Then Claude** (the platform's, on its account, no scope or key needed) answers with the likely cause, a patch as a
327
+ unified diff, and how to check it. Check the patch before you apply it; nothing is changed for you.
328
+ - **In Claude Code,** the MCP server's `error_brief` tool gives it the same brief, so it can fix the code in your repo
329
+ directly (ask: "fix error 3fa1c2d9").
330
+
331
+ It needs the version's **source maps**: the templates build them (`productionBrowserSourceMaps` in next.config.ts,
332
+ `build.sourcemap: 'hidden'` in vite.config.ts), and `hawk-app deploy` sends every `.map` file of the build to the
333
+ platform's private bucket instead of the CDN, so your source is never public. Without maps the stack stays minified and
334
+ the brief says so. Errors of versions deployed before keep their minified stacks; the next deploy maps the new ones.
335
+
301
336
  ## Accessibility check
302
337
 
303
338
  `hawk-app deploy` runs axe-core (WCAG 2.1 A/AA and best practices) over up to 10 built pages and sends the result with
@@ -338,14 +373,40 @@ instead of guessing:
338
373
  what fails review. Its project section matches the app's template, or, for an app set up with `init`, says what the
339
374
  project has to do itself.
340
375
  - **`CLAUDE.md`** with `@AGENTS.md`, so Claude Code reads the same file.
341
-
342
- - **`.mcp.json`** (Claude Code) and **`.vscode/mcp.json`** (VS Code / Copilot), which start the CLI's MCP server (see
343
- below) from `node_modules`.
376
+ - **`.vscode/mcp.json`** (VS Code / Copilot), which starts the CLI's MCP server (see below) from `node_modules`.
344
377
 
345
378
  The CLI owns the part of AGENTS.md between its `<!-- hawk-app:start … -->` and `<!-- hawk-app:end -->` markers, and
346
379
  `npx hawk-app upgrade` refreshes it when a new CLI changes it. Write your own notes outside the markers; they're kept.
347
- An existing AGENTS.md or CLAUDE.md is never replaced: the CLI's part (or the import) is added to it. An existing MCP
348
- settings file gets the `hawk-apps` server added (one with comments is left alone: add it yourself).
380
+ An existing AGENTS.md or CLAUDE.md is never replaced: the CLI's part (or the import) is added to it. An existing
381
+ `.vscode/mcp.json` gets the `hawk-apps` server added (one with comments is left alone: add it yourself).
382
+
383
+ ### Claude Code plugin (`hawk-app claude`)
384
+
385
+ Claude Code gets the MCP server, and skills for the common jobs, from the **Hawk Apps plugin**, installed once per
386
+ machine (not per app):
387
+
388
+ ```bash
389
+ npx @bridgeline-digital/hawk-app-cli@latest claude
390
+ ```
391
+
392
+ It copies the plugin, which ships in this package, next to your login (`%APPDATA%\hawk-app\claude-plugin` on Windows,
393
+ `~/.config/hawk-app/claude-plugin` elsewhere) and adds it to Claude Code as a local marketplace with `claude plugin`
394
+ when that command is on the PATH. Otherwise it prints the two commands to type in the Claude Code panel:
395
+ `/plugin marketplace add <folder>` and `/plugin install hawk-apps@hawk-apps`. No repository access is needed. Run it
396
+ again to update: the plugin carries the CLI's version, so Claude Code takes the new one (`/reload-plugins` in an open
397
+ session). Engineers can also add the repository itself as the marketplace (`.claude-plugin/marketplace.json` at its root).
398
+
399
+ | Skill | What it does |
400
+ | --- | --- |
401
+ | `/hawk-apps:new-screen` | A new page, view or panel from the UI kit: its components, the data's bridge calls and scopes, the states every screen needs, then `validate`. |
402
+ | `/hawk-apps:add-query` | A query in `queries/`: report first if one fits, tables and columns from `data_tables`/`data_describe`, the partitions rule, the params file, `validate`; client rows never come into the chat. |
403
+ | `/hawk-apps:add-function` | A function, job or schedule: the declaration, the handler, secrets set by you in a terminal, errors, `validate`. |
404
+ | `/hawk-apps:prepare-review` | The approvers' checks (bundle, dependencies, accessibility, outside origins, scopes, queries, functions): fixes them or says what's left. |
405
+
406
+ Claude also picks a skill on its own when your request matches it. The plugin's MCP server starts in the open folder,
407
+ from the app's own CLI (`node_modules`) when it has one, else the released CLI through `npx`. The CLI no longer writes
408
+ `.mcp.json` for Claude Code; `upgrade` takes the old `hawk-apps` entry out of it (other servers are kept), so the tools
409
+ aren't listed twice.
349
410
 
350
411
  ### MCP server (`hawk-app mcp`)
351
412
 
@@ -360,6 +421,7 @@ Protocol](https://modelcontextprotocol.io)), so they can look things up and chec
360
421
  | `doctor` | `hawk-app doctor`: what's wrong with the setup, and how to fix it. |
361
422
  | `versions`, `why` | Every version with its status and aliases; what a client sees of the app, and why. |
362
423
  | `errors`, `usage` | The app's page errors; its opens, users and clients per day. |
424
+ | `error_brief` | One error's brief (`errors --explain <id> --no-ai`): the stack mapped to the source with the code, the failed platform calls, what changed since the last version without it. For fixing it in the repo. |
363
425
  | `ui_components` | The UI kit's components for the app's template (next-mui: the provider, components and theme; the others: the stylesheet classes and elements), or what's said about one, from the kit's README, with a link to the live [gallery](https://developers.hawksearchapps.com/reference/ui-kit-gallery/). |
364
426
  | `analytics_reports` | The analytics API's reports for `app.analytics.report()`, with one report's columns, filters and an example call, read from the developer portal's dev spec (public; no client data). |
365
427
  | `hawksearch_api` | HawkSearch's API reference (developerdocs.hawksearch.com): an operation's parameters, request and response fields, the bridge call (`app.dashboard.*`, `app.hawksearch.*`) and its scope, or why apps can't call it (public; no client data). |
@@ -369,17 +431,17 @@ Protocol](https://modelcontextprotocol.io)), so they can look things up and chec
369
431
  | `deploy` | `hawk-app deploy` (the folder is optional; `alias`, and `dryRun` to only show what hawk-app.json would change). Marked destructive, so the assistant asks you first. |
370
432
  | `clients`, `clients_add` | The app's clients, by id. Say "deploy to OTC": `clients_add` finds the matching clients and shows *you* a form to pick one; the assistant only hears whether a client was added. Marked destructive too. Removing clients isn't a tool. |
371
433
 
372
- It also has **prompts**, ready-made tasks that use the tools. Claude Code lists them as slash commands (type `/` and
373
- `hawk`):
434
+ It also has **prompts**, ready-made tasks that use the tools (in Claude Code, the plugin's skills cover the same
435
+ jobs; its slash commands list the prompts too, under the plugin's server):
374
436
 
375
- | Prompt | In Claude Code | What it does |
376
- | --- | --- | --- |
377
- | `check-before-submit` | `/mcp__hawk-apps__check-before-submit` | Validates (with the build check), reads the status and review checks, and explains how to fix what an approver would have to force approve. |
378
- | `add-feature` | `/mcp__hawk-apps__add-feature "<what to build>"` | Finds the bridge calls, scopes and showcase examples it needs, says what the review will see, makes the change and validates it. |
379
- | `explain-app` | `/mcp__hawk-apps__explain-app` | What the app does, its scopes (unused or missing ones too) and where it stands in review. |
380
- | `deploy-to-client` | `/mcp__hawk-apps__deploy-to-client "<client name>"` | Deploys to dev and gives a client the app; you pick the client from the matches for its name. |
437
+ | Prompt | What it does |
438
+ | --- | --- |
439
+ | `check-before-submit` | Validates (with the build check), reads the status and review checks, and explains how to fix what an approver would have to force approve. |
440
+ | `add-feature` | Finds the bridge calls, scopes and showcase examples it needs, says what the review will see, makes the change and validates it. |
441
+ | `explain-app` | What the app does, its scopes (unused or missing ones too) and where it stands in review. |
442
+ | `deploy-to-client` | Deploys to dev and gives a client the app; you pick the client from the matches for its name. |
381
443
 
382
- Each takes an optional `dir` for a workspace with several apps. In VS Code with Copilot they're under `/` too.
444
+ Each takes an optional `dir` for a workspace with several apps. In VS Code with Copilot they're under `/`.
383
445
 
384
446
  - **Dev only.** Every tool runs with `--env dev`; nothing reaches prod. Approvals, secrets, removing clients and logins
385
447
  aren't tools.
@@ -392,9 +454,10 @@ Each takes an optional `dir` for a workspace with several apps. In VS Code with
392
454
  critical vulnerabilities) the tool stops and tells the assistant, which has to ask you and call again with `force`.
393
455
  - **Several apps in one workspace?** Each tool takes `dir`, the app's folder relative to the workspace.
394
456
 
395
- `create` and `init` add it to `.mcp.json` and `.vscode/mcp.json`, started as
457
+ `create` and `init` add it to `.vscode/mcp.json`, started as
396
458
  `node node_modules/@bridgeline-digital/hawk-app-cli/bin/hawk-app.mjs mcp` (the CLI must be a dev dependency;
397
- `validate` warns when it isn't). For Cursor, copy the `mcpServers` entry of `.mcp.json` to `.cursor/mcp.json`.
459
+ `validate` warns when it isn't); Claude Code gets it from the plugin (above). For Cursor, add the same command to
460
+ `.cursor/mcp.json` under `mcpServers`.
398
461
 
399
462
  ## Clients
400
463
 
@@ -614,6 +677,107 @@ with 6 fields (`minutes hours day-of-month month day-of-week year`, no `cron( )`
614
677
  scope. `schedules` is a setting sent with each deploy: on a released app a change waits for review like scopes,
615
678
  and `hawk-app status` lists the registered schedules.
616
679
 
680
+ `events` run a job function when something happens for a client, so the app doesn't need a schedule to notice it.
681
+ `client.added` runs when a client's users get the app (a store request approved, the client added to the app's list,
682
+ or the app released to the clients on its list), `config.changed` when a config value changes for a client (its own,
683
+ or a global one), `version.released` when another version reaches a client that had the app, and, read from HawkSearch
684
+ once a minute, `hawksearch.changed` (a dashboard change; `"sections": ["synonym", …]` to follow only some) and
685
+ `index.rebuilt` (a rebuild finished):
686
+
687
+ ```jsonc
688
+ "functions": { "welcome": { "job": true }, "migrate": { "job": true } },
689
+ "events": { "client.added": { "function": "welcome" }, "version.released": { "function": "migrate" } }
690
+ ```
691
+
692
+ The run is a job for that client, without a site or HawkSearch connection, except the HawkSearch events, which get the
693
+ engine and the client's connection (the bridge README's **Events** section has the details). `hawk-app validate` checks the event names, that each `function` is a job function of the same
694
+ `hawk-app.json`, and the `functions:invoke` scope; `events` is reviewed like scopes, and `hawk-app status` lists them.
695
+
696
+ `api` turns functions into endpoints the client's own systems call (a developer's script, a job on their side) with
697
+ their engine's HawkSearch site API key, once Bridgeline turns API access on for the client in HawkSearch (the app's
698
+ kebab menu › API access, which shows each endpoint's URL and a curl):
699
+
700
+ ```jsonc
701
+ "functions": { "design-html": {} },
702
+ "api": { "design-html": { "function": "design-html", "method": "GET", "description": "A saved design's email HTML" } }
703
+ ```
704
+
705
+ The function isn't a job (the caller waits); its input is the query string (GET) or the JSON body (POST), and
706
+ `apiResponse()` from the bridge's `/server` entry answers HTML or any other type (the bridge README's **Client API**
707
+ section). `hawk-app validate` checks the names (at most 10), that each `function` is a function of the same
708
+ `hawk-app.json` without `"job": true`, the method (GET or POST) and the `functions:invoke` scope; `api` is reviewed like
709
+ scopes, and `hawk-app status` lists the endpoints.
710
+
711
+ ### Actions (start the app from HawkSearch's screens)
712
+
713
+ `actions` put an app where the work is: a button in HawkSearch's own pages that opens one of the app's views in a
714
+ dialog, with values read from the page as the view's params. The app says where with **targets**: the page (a regular
715
+ expression on its path), a CSS selector for the things (one button per match), where the button goes, and how each
716
+ param is read:
717
+
718
+ ```jsonc
719
+ "views": { "fix": { "path": "views/fix", "title": "Fix a search", "params": ["keyword"] } },
720
+ "actions": {
721
+ "fix-keyword": {
722
+ "label": "Fix with Zero Results Fixer",
723
+ "view": "fix",
724
+ "targets": { "hawksearch": [{
725
+ "path": "/admin/advanced-analytics/top-queries/?$",
726
+ "selector": "bl-table tbody.table__body > tr.table__row",
727
+ "anchor": "td.table__cell:first-child",
728
+ "params": { "keyword": { "selector": "td.table__cell:first-child" } }
729
+ }] }
730
+ }
731
+ }
732
+ ```
733
+
734
+ Deploy; once the version is approved, the buttons appear in HawkSearch with no change there. `actions` always needs an
735
+ approver (never auto-approve): reviewers see each target and what it reads. Pick stable selectors (ids, `data-*`
736
+ attributes, element names): if HawkSearch changes its markup, deploy new targets. A param can also name a `data-*`
737
+ attribute (`"data": "hawk-keyword"`), used as is when the page has it, with its selector and pattern as the fallback. The view reads
738
+ `app.context.view.params` and `app.context.view.action`, and calls `app.action.done({ changed })` to close the dialog
739
+ (the bridge README's **Actions**, with every target setting). `hawk-app validate` checks the targets (paths and
740
+ patterns are regular expressions, params the view declares); `hawk-app status` lists the actions. `hawk-app dev` has an
741
+ **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).
743
+
744
+ ### Testing functions (`hawk-app test`)
745
+
746
+ Functions can be tested without deploying. Put tests next to them as `functions/<name>.test.ts` (Node's own
747
+ `node:test` and `node:assert`: nothing to install) and run `npx hawk-app test` (or `npm run app:test`):
748
+
749
+ ```ts
750
+ // functions/design-html.test.ts
751
+ import assert from 'node:assert/strict';
752
+ import { test } from 'node:test';
753
+ import { runFunction } from '@bridgeline-digital/hawk-app-cli/testing';
754
+
755
+ test('answers the design as JSON', async () => {
756
+ const run = await runFunction('design-html', {
757
+ trigger: 'api', // call (default), job, schedule, event or api
758
+ input: { designId: 'spring' },
759
+ storage: { 'design#spring': design }, // storage in memory
760
+ answers: [{ method: 'POST', path: 'hawksearch/recommendations', body: recommendations }],
761
+ });
762
+ assert.equal(run.error, null, run.logs);
763
+ assert.equal(run.result.subject, 'New for spring');
764
+ });
765
+ ```
766
+
767
+ - `runFunction(name, options)` bundles the function exactly as `hawk-app deploy` does and runs it in its own Node
768
+ process with the event the platform sends (`input`, `trigger`, `client`, `engine`, `user`, `secrets`,
769
+ `clientSecrets`, `config`), with its `timeoutSeconds`. It answers `{ result, error, calls, progress, storage, logs }`:
770
+ what it returned or threw, every platform call it made (with its body and what answered it), `ctx.job.progress`
771
+ reports, the storage afterwards, and its console output.
772
+ - Its platform calls go to a fake platform: `answers` first, then storage and config in memory (when the test gives
773
+ `storage` or `config`), then a **recording** (`recording: 'spring'` reads `functions/__recordings__/spring.json`).
774
+ A call nothing answers fails with 501 `not_recorded`.
775
+ - `hawk-app test --record` makes the recordings: calls nothing else answers go to the platform once, as the test's
776
+ `client` (and `engine`; `HAWKSEARCH_API_KEY` for HawkSearch), and are saved. Recordings hold real answers: review them
777
+ before committing. They are never deployed with the function's source.
778
+ - `hawk-app test <filter>` runs only the test files whose path contains the filter. Tests aren't functions: they are
779
+ never deployed as one.
780
+
617
781
  ## Email
618
782
 
619
783
  Apps with the `email:send` scope can send email with `app.email.send(...)` or, from a function,
@@ -690,7 +854,7 @@ with it is never auto-approved. Prompts and answers are never logged or stored.
690
854
  the Apps page (see [Versions and aliases](#versions-and-aliases)).
691
855
  - **Released apps.** Every deploy is a new version. Clients stay on the approved version until the new one is approved in
692
856
  HawkSearch (staff can open it there first). Changes to settings in `hawk-app.json` (name, description,
693
- icon, category, version, visibility, hosting, scopes, `systemParameters`, `schedules`, `config`, `externalOrigins`, `listed`, `minProtocolVersion`) wait for approval
857
+ icon, category, version, visibility, hosting, scopes, `systemParameters`, `schedules`, `events`, `api`, `actions`, `config`, `externalOrigins`, `listed`, `minProtocolVersion`) wait for approval
694
858
  the same way. For `config` only the declarations are reviewed: its values are set at any time without review.
695
859
  - **Functions.** Engineering enables functions per app. A version whose functions changed (added, removed, new code,
696
860
  dependencies or settings) always waits for review, even with auto-approve; reviewers see each function's settings,
package/agents/AGENTS.md CHANGED
@@ -50,11 +50,14 @@ app.onContextChange(render); // the user switched client, engine or dates
50
50
  | Call | Scope |
51
51
  | --- | --- |
52
52
  | `app.analytics.report('queries', { limit, startDate, endDate, filters })` | `reports:read` |
53
- | `app.dashboard.get('Synonym')`: HawkSearch dashboard API, no API version in the path | `hawksearch:read` |
53
+ | `app.dashboard.get('Synonym')`: HawkSearch dashboard API, no API version in the path. Typed routes: `app.dashboard.resource('Synonym').list()/get(id)` (reads) and `.create/update/delete` (`hawksearch:write`), models as `HawkSearchModel<'Synonym'>` | `hawksearch:read` |
54
54
  | `app.dashboard.post/put/delete(path, body)`: merchandising and relevancy resources only, never retried | `hawksearch:write` |
55
- | `app.hawksearch.search(...)`, `app.hawksearch.autocomplete(...)` on the user's engine | `hawksearch:search` |
55
+ | `app.hawksearch.pinning.set(itemPinId, docId, isPinned)`, `.order(itemPinId, docIds)`: the Item Pin rule a search applied (its `ItemPinId`), saved as the user | `hawksearch:write` |
56
+ | `app.hawksearch.search(...)`, `app.hawksearch.autocomplete(...)` on the user's engine; preview: `IsInPreview`, `SmartBar`, `ClientData.PreviewBuckets`, `IndexName`, `ExplainDocId` | `hawksearch:search` |
57
+ | `app.hawksearch.preview.settings/indexes/item/explain`: the preview page's visitor targets, indexes, item details, keyword explain | `hawksearch:read` |
58
+ | `app.hawkai.agents/agent/smartResponses/smartResponse`: the client's HawkAI agents and smart responses (names and settings); `runSmartResponse(id, { query, search })`: its answer for a (preview) search; `assistant(agentId)`: the values for HawkAI's assistant widget | `hawkai:read` |
56
59
  | `app.data.query('name', params)`: your SQL in `queries/<name>.sql` + `.json` | `data:read` |
57
- | `app.functions.call('name', input)`, `.start/.wait/.job` for jobs: `functions/<name>.ts` exporting `handler` | `functions:invoke` |
60
+ | `app.functions.call('name', input)`, `.start/.wait/.job` for jobs: `functions/<name>.ts` exporting `handler`. `schedules` and `events` in hawk-app.json run job functions on a cron, or when something happens for a client (`client.added`, `config.changed`, `version.released`, `hawksearch.changed` with optional `sections`, `index.rebuilt`; input types `HawkAppEvent` from the bridge's `/server`). `api` in hawk-app.json makes a function (not a job) an endpoint the client's own systems call with their HawkSearch site API key (`{ "<name>": { "function", "method": "GET" \| "POST" } }`; `apiResponse(body, { contentType })` for HTML). Test functions without deploying: `functions/<name>.test.ts` with `node:test` and `runFunction` from `@bridgeline-digital/hawk-app-cli/testing` (storage, answers, recordings), run by `npx hawk-app test` | `functions:invoke` |
58
61
  | `app.email.send(...)` | `email:send` |
59
62
  | `app.storage.get/put/delete/list/batch/usage`: the app's own JSON data per client (350 KB a value, `ttlDays`, `ifVersion`); `app.storage.files.upload/url/download/list/delete`: its files (100 MB each) | `storage` |
60
63
  | `app.hawksearch.systemParameters.list/get/set`: names listed in `systemParameters` | `hawksearch:sysparams:write` |
@@ -92,21 +95,23 @@ https://www.npmjs.com/package/@bridgeline-digital/hawk-app-bridge. Its TypeScrip
92
95
 
93
96
  ## MCP server
94
97
 
95
- `.mcp.json` (Claude Code) and `.vscode/mcp.json` (VS Code) start the CLI's MCP server, `hawk-apps`, from
96
- `node_modules` (`npm install` first). Its tools: `docs` (the bridge, UI kit, CLI, `hawk-app.json` schema and the
98
+ The CLI's MCP server, `hawk-apps`: Claude Code gets it from the Hawk Apps plugin (`npx @bridgeline-digital/hawk-app-cli@latest
99
+ claude`, once per machine; it also adds the skills `/hawk-apps:new-screen`, `/hawk-apps:add-query`,
100
+ `/hawk-apps:add-function` and `/hawk-apps:prepare-review`), and VS Code from `.vscode/mcp.json`; both start this app's
101
+ copy in `node_modules` (`npm install` first). Its tools: `docs` (the bridge, UI kit, CLI, `hawk-app.json` schema and the
97
102
  showcase app, which has working code for every feature), `ui_components` (the UI kit's components for this app's
98
103
  template: use them before writing your own), `validate`, `status` (with the review checks approvers will see),
99
104
  `analytics_reports` (every report `app.analytics.report()` can call, with its columns and filters),
100
105
  `hawksearch_api` (HawkSearch's dashboard, search, recommendation and indexing operations: fields, the bridge call, its scope),
101
106
  `data_tables` and `data_describe` (names and columns, never data), `logs`, `init`, `upgrade`,
102
- `deploy` (with `alias`, and `dryRun` to only show what hawk-app.json would change), `versions`, `why` (what a client sees, and why), `doctor` (what's wrong with the setup), `errors`, `usage`, and `clients` and `clients_add`, which only ever use the dev platform. Prefer them to guessing. When the
107
+ `deploy` (with `alias`, and `dryRun` to only show what hawk-app.json would change), `versions`, `why` (what a client sees, and why), `doctor` (what's wrong with the setup), `errors`, `error_brief` (one error mapped to the source, with the failed calls and what changed since the last version without it: use it to fix production errors here), `usage`, and `clients` and `clients_add`, which only ever use the dev platform. Prefer them to guessing. When the
103
108
  user names a client ("deploy to OTC"), call `clients_add` with their words: it shows them a form to pick the exact
104
109
  client. With `alias` (`hawk-app clients add <id> --alias review`) the client gets that alias's version: on a version that
105
110
  isn't approved, only Bridgeline staff in it see the app. Only approvers change what a client whose users see the app
106
111
  gets. `hawk-app deploy out --alias staging` deploys off the main line: the version carries this hawk-app.json, `staging`
107
112
  points at it, and the version under review isn't touched. A plain `hawk-app deploy` sends hawk-app.json first, so scope
108
113
  changes go with the deploy. Client names aren't shared with you; don't ask for them in the chat. Its prompts (`check-before-submit`, `add-feature`,
109
- `explain-app`, `deploy-to-client`; in Claude Code `/mcp__hawk-apps__<name>`) are ready-made tasks that use them.
114
+ `explain-app`, `deploy-to-client`) are ready-made tasks that use them; in Claude Code prefer the plugin's skills.
110
115
 
111
116
  ## Icon
112
117
 
@@ -136,6 +141,24 @@ Each view is a route, e.g. `src/app/views/preview/page.tsx` for `"path": "views/
136
141
  `{ cache: 'no-store' }` on a call that writes (e.g. an analytics report that records something), or
137
142
  `{ cache: 'reload' }` to refresh. The full app page is never cached.
138
143
 
144
+ **Say what each page shows**: HawkSearch's "Explain this page" button explains the app's page from it (no scope).
145
+ With the UI kit: `usePageContext(rows ? { page: '<the page>', dateRange, totals, top10 } : null)`; otherwise
146
+ `app.assistant.setContext({ ... })` whenever the data or filters change. Plain names and numbers of what's on screen,
147
+ at most 50 KB; summarize long tables. `hawk-app validate` warns when an app never sets one.
148
+
149
+ **Export as PDF** (only on pages worth keeping: a report, a summary): `usePdfExport({ fileName })` from the UI kit
150
+ (`false` while loading), or `app.setPdfExport({ fileName })` and `app.setPdfExport(null)` when the page leaves. The
151
+ host shows a PDF icon and downloads the whole page; mark buttons and filters `data-hawk-pdf-ignore`.
152
+
153
+ **Actions** (`actions` in `hawk-app.json`) add a button to HawkSearch's own pages that opens a view in a dialog:
154
+ `{ "<name>": { "label": "Fix with …", "view": "fix", "targets": { "hawksearch": [{ "path": "<regex on the page path>",
155
+ "selector": "<CSS: each thing>", "anchor"?: "<CSS inside it>", "position"?: "after", "params": { "keyword": { "data"?,
156
+ "selector"?, "attribute"?, "pattern"? } } }] } } }`. `"data": "keyword"` reads `data-keyword` as is when the page has it,
157
+ else the rest of the reader applies. Prefer stable selectors (ids, `data-*`, element names); the view must list the
158
+ params in its `params` and handle them missing. The view reads `app.context.view.params` (and `context.view.action`:
159
+ `{ name }`), its toasts show, and `app.action.done({ changed: true })` closes the dialog once the work is saved. Actions
160
+ always need an approver. Try it with `hawk-app dev`'s Action menu.
161
+
139
162
  A slot can also hold the **entire app** (its own page, with no `view` in the context): it then runs inside another
140
163
  page, at the slot's height, so don't rely on the full window either.
141
164
 
@@ -169,7 +192,8 @@ npx hawk-app versions # every version with its status and aliases
169
192
  npx hawk-app open # the app's page in HawkSearch (review, submit for approval)
170
193
  npx hawk-app logs <function> # a function's logs
171
194
  npx hawk-app config list # config values (config set <key> <value> [--client <id>], config unset <key>)
172
- npx hawk-app errors # errors in the app's pages (deployed versions), the most frequent first
195
+ npx hawk-app errors # errors in the app's pages (deployed versions), the most frequent first, with [id]s
196
+ npx hawk-app errors --explain <id> # one error mapped to the source, the failed calls, what changed; then a likely fix
173
197
  npx hawk-app usage # opens, users and clients per day
174
198
  npx hawk-app upgrade # bring the app (and this file) up to the CLI's current template
175
199
  npx hawk-app upgrade --channel latest # a dev app (bridge/CLI on the `dev` tag) to the released versions
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "hawk-apps",
3
+ "owner": { "name": "Bridgeline Digital" },
4
+ "metadata": {
5
+ "description": "Hawk Apps for Claude Code: the hawk-apps MCP server and skills for building, checking and shipping Hawk Apps."
6
+ },
7
+ "plugins": [
8
+ {
9
+ "name": "hawk-apps",
10
+ "source": "./hawk-apps",
11
+ "description": "Build Hawk Apps with Claude Code: the hawk-apps MCP server (docs, UI kit, validate, deploy to dev) and skills for new screens, queries, functions and review."
12
+ }
13
+ ]
14
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "hawk-apps",
3
+ "version": "0.1.0",
4
+ "description": "Build Hawk Apps with Claude Code: the hawk-apps MCP server (docs, UI kit, validate, deploy to dev) and skills for new screens, queries, functions and review.",
5
+ "author": { "name": "Bridgeline Digital" },
6
+ "homepage": "https://developers.hawksearchapps.com/start/with-claude/",
7
+ "keywords": ["hawk-apps", "hawksearch"]
8
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "mcpServers": {
3
+ "hawk-apps": {
4
+ "command": "node",
5
+ "args": ["${CLAUDE_PLUGIN_ROOT}/scripts/mcp.mjs"]
6
+ }
7
+ }
8
+ }
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+ // Starts the Hawk Apps MCP server (`hawk-app mcp`) for Claude Code, in the open project: the project's own CLI when it
3
+ // has one (a dev dependency, so the tools match the app's CLI version and channel), else the released CLI through npx.
4
+ // stdio is passed straight through: it is the MCP protocol.
5
+ import { spawn } from 'node:child_process';
6
+ import { existsSync } from 'node:fs';
7
+ import { dirname, join, resolve } from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ const CLI_BIN = join('node_modules', '@bridgeline-digital', 'hawk-app-cli', 'bin', 'hawk-app.mjs');
11
+
12
+ /** The CLI installed in the project or a folder above it (a workspace of apps), or null. */
13
+ export function findProjectCli(start) {
14
+ for (let dir = resolve(start); ; dir = dirname(dir)) {
15
+ const bin = join(dir, CLI_BIN);
16
+ if (existsSync(bin)) return bin;
17
+ if (dirname(dir) === dir) return null;
18
+ }
19
+ }
20
+
21
+ function run() {
22
+ const project = process.env.CLAUDE_PROJECT_DIR || process.cwd();
23
+ const local = findProjectCli(project);
24
+ const child = local
25
+ ? spawn(process.execPath, [local, 'mcp'], { cwd: project, stdio: 'inherit' })
26
+ : spawn('npx', ['--yes', '@bridgeline-digital/hawk-app-cli@latest', 'mcp'], { cwd: project, stdio: 'inherit', shell: process.platform === 'win32' });
27
+ child.on('error', (error) => {
28
+ process.stderr.write(`hawk-apps: could not start the MCP server (${error.message}). Is Node.js installed?\n`);
29
+ process.exit(1);
30
+ });
31
+ child.on('exit', (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
32
+ for (const signal of ['SIGINT', 'SIGTERM']) process.on(signal, () => child.kill(signal));
33
+ }
34
+
35
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) run();
@@ -0,0 +1,46 @@
1
+ ---
2
+ name: add-function
3
+ description: Add or change a server-side function (functions/<name>.ts, AWS Lambda), a background job, a schedule, an event handler or a client API endpoint in a Hawk App, with tests. Use when the app needs server code, a third-party API with a secret key, work longer than a page should wait, something on a timer, setup when a client gets the app, or data the client's own systems fetch.
4
+ ---
5
+
6
+ # Add a function
7
+
8
+ Functions are the app's own server code (Node 22 Lambda), shipped with each version. **Engineering must enable
9
+ functions for the app**, and a version whose functions changed always waits for review. Before writing one, check
10
+ whether the bridge already does it from the page (`docs`): no function is simpler.
11
+
12
+ 1. **Scope and declaration.** Tell the user, then add `functions:invoke` to `scopes` and declare the function in
13
+ `hawk-app.json`:
14
+ ```jsonc
15
+ "functions": { "sync-feed": { "timeoutSeconds": 20, "memoryMb": 512, "secrets": ["FEED_API_KEY"] } }
16
+ ```
17
+ Names: lowercase letters, digits, single dashes, up to 40. `timeoutSeconds` 1–25 (1–900 with `"job": true`),
18
+ `memoryMb` 128–1024, at most 10 functions.
19
+ 2. **Write `functions/<name>.ts`** exporting `handler`:
20
+ ```ts
21
+ import type { HawkFunction } from '@bridgeline-digital/hawk-app-bridge/server';
22
+ export const handler: HawkFunction<In, Out> = async (input, ctx) => { … };
23
+ ```
24
+ Use `ctx.platform` (the same APIs as `app`), `ctx.client.id`, `ctx.user`, `ctx.config`, `ctx.secrets.NAME`. Stateless:
25
+ keep state in `ctx.platform.storage`. Look up `ctx` and the errors with `docs` ("Functions").
26
+ 3. **Secrets** are never in code, `hawk-app.json` or the chat. Tell the user to run `npx hawk-app secrets set NAME`
27
+ themselves (hidden input). A value each client sets for itself is a `clientSecrets` entry instead.
28
+ 4. **Long work** is a job (`"job": true`, up to 900 s): the app calls `app.functions.start(name, input)` and
29
+ `app.functions.wait(jobId)`. **On a timer** it's a `schedules` entry (a job function, a 6-field EventBridge cron at
30
+ most hourly, an IANA `timezone`) that users turn on per client with `app.schedules.enable(name)`. **When something
31
+ happens for a client** it's an `events` entry (`"<event>": { "function": "<job>" }`): `client.added` (it got the
32
+ app), `config.changed` (its config changed), `version.released` (a new version reached it): these run without a site
33
+ or HawkSearch connection, so they can't call HawkSearch. `hawksearch.changed` (a dashboard change, `sections` to
34
+ narrow it) and `index.rebuilt` come from HawkSearch within 1–2 minutes and run with the engine and connection; keep
35
+ them idempotent.
36
+ 5. **Call it** from the app with `app.functions.call('<name>', input)` and handle `function_failed`,
37
+ `function_timeout`, `secret_missing` and `functions_not_enabled`. **For the client's own systems** (a script, a job
38
+ on their side) make it an endpoint instead: `"api": { "<name>": { "function": "<function>", "method": "GET" } }`
39
+ (not a job; input from the query or JSON body; `apiResponse(body, { contentType })` from `/server` for HTML).
40
+ Bridgeline turns API access on per client; the client calls it with its HawkSearch site API key.
41
+ 6. **Test it** without deploying: `functions/<name>.test.ts` with `node:test` and `runFunction` from
42
+ `@bridgeline-digital/hawk-app-cli/testing` (input, trigger, `storage` in memory, `answers` for platform calls,
43
+ recordings), then `npx hawk-app test`. A client API endpoint also runs locally under `hawk-app dev` at
44
+ `http://localhost:3200/v1/clients/<id>/apps/<slug>/api/<name>`.
45
+ 7. **Check it.** Run `validate`. Called from the app, functions run from the **deployed** version; deploy only when the
46
+ user asks. After a deploy, `logs` shows the function's output.
@@ -0,0 +1,31 @@
1
+ ---
2
+ name: add-query
3
+ description: Add or change a SQL query (queries/<name>.sql + .json) that a Hawk App runs on the client's analytics data with app.data.query. Use when the user wants data no analytics report gives, a custom aggregate, or to fix a query in a Hawk App.
4
+ ---
5
+
6
+ # Add a query
7
+
8
+ Queries are the app's own SQL, run by the platform (Athena) on the current client's analytics tables. They ship with
9
+ each version and approvers review them. Check first whether an analytics report already gives the data (the hawk-apps
10
+ `analytics_reports` tool): a report needs no SQL and no review.
11
+
12
+ 1. **Scope.** `data:read` in `hawk-app.json` `scopes` (elevated: the app is never auto-approved). Tell the user before
13
+ adding it.
14
+ 2. **Tables and columns.** `data_tables` (which tables exist, which are partitioned, which are sensitive) and
15
+ `data_describe <table>`. Never guess a column. Sensitive tables (visitor-level data) need Engineering's opt-in:
16
+ say so instead of using one.
17
+ 3. **Write `queries/<name>.sql`** next to `hawk-app.json` (not in the build folder). Name: lowercase letters, digits,
18
+ single dashes.
19
+ - one `SELECT` or `WITH … SELECT`, no `;`;
20
+ - a `?` for each parameter, in order (`IN ?` for a `string[]`);
21
+ - every query over dated data filters by a date range **and** has `{{partitions(from, to)}}` (params of type `date`);
22
+ - name every column (no `SELECT *`), and `LIMIT` every list.
23
+ 4. **Write `queries/<name>.json`**: `{ "params": [{ "name": "from", "type": "date", "required": true }, …] }`. Types:
24
+ `string`, `int`, `decimal`, `date`, `timestamp`, `boolean`, `string[]`; optional `required`, `default`,
25
+ `min`/`max`, `maxItems`.
26
+ 5. **Call it** with `app.data.query('<name>', { from, to })` (or `ctx.platform.data.query` in a function). Look up the
27
+ result shape and errors with `docs` ("data.query"). Handle a client without analytics.
28
+ 6. **Check it.** Run `validate`. To try the SQL, tell the user to run
29
+ `npx hawk-app data run queries/<name>.sql --client <test client id> --param from=… --param to=…` in a terminal: its rows
30
+ are client data and must not come into the chat. Queries run only from a **deployed** version, so the app sees a
31
+ new or changed query after a deploy (only when the user asks).
@@ -0,0 +1,36 @@
1
+ ---
2
+ name: new-screen
3
+ description: Build a new screen, page, view or panel in a Hawk App with the Hawk Apps UI kit (Spark's look in the host's colors). Use when the user asks for a new page, tab, dashboard, table, chart, form or preview card in a Hawk App (a folder with hawk-app.json).
4
+ ---
5
+
6
+ # New screen from the UI kit
7
+
8
+ A Hawk App's screens use the Hawk Apps UI kit (`@bridgeline-digital/hawk-app-ui`), so they look like Spark in the host's
9
+ colors and light/dark mode. Never restyle the kit or add another font.
10
+
11
+ 1. **Read the app.** `hawk-app.json` (its `template`: `next-mui` uses the kit's React + MUI provider and components;
12
+ `next`, `react`, `preact` and `vanilla` use its stylesheet and custom elements) and the CLI's part of `AGENTS.md`.
13
+ 2. **Find the components.** Call the hawk-apps `ui_components` tool for this app's template, then for each component
14
+ you plan to use. Use what the kit has (tables, cards, filters, empty states, dialogs) before writing your own. For
15
+ your own styles use the kit's `--hawk-ui-*` variables.
16
+ 3. **Find the data.** For each number or list on the screen, pick the bridge call and look it up with `docs` (options,
17
+ limits, error codes; the showcase sections have working code):
18
+ - analytics reports: `analytics_reports` gives the report's columns and filters;
19
+ - the client's HawkSearch setup or searches: `hawksearch_api`;
20
+ - your own SQL: the **add-query** skill;
21
+ - server work or secrets: the **add-function** skill.
22
+ Tell the user which scopes the screen needs before adding them to `hawk-app.json`.
23
+ 4. **Build it** in the app's style:
24
+ - read the client, engine and dates from `app.context` and re-load on `app.onContextChange`;
25
+ - when `app.context.tenant.analyticsProvisioned === false`, show a message instead of calling analytics or queries;
26
+ - loading, empty and error states (`HawkApiError.code`), no page heading (the host shows the app's name);
27
+ - dialogs in the page (never `window.confirm`/`alert`), placed in the visible part of the frame;
28
+ - accessible: labelled fields, `alt` on images, one `<main>`, headings in order.
29
+ 5. **A view** (a preview card or an embeddable slot) is a route declared in `views` in `hawk-app.json`: keep it small,
30
+ read stored results, transparent background, height from the content. See the Views section of `AGENTS.md`.
31
+ **Opened from HawkSearch's own pages** (a button by a search term in a report, on an AI Advisor insight…), it's an
32
+ `actions` entry: the view, and `targets` per host (a regex on the page path, CSS selectors, how each param is read;
33
+ the bridge README's **Actions**). The view reads `app.context.view.params`, handles them missing, and calls
34
+ `app.action.done({ changed: true })` when the work is saved. Try it with `npm run app:dev`'s Action menu.
35
+ 6. **Check it.** Run the hawk-apps `validate` tool (with `build: true` when the build setup changed) and fix what it
36
+ says. Offer `npm run app:dev` to see it locally. Deploy only when the user asks.