@bridgeline-digital/hawk-app-cli 0.1.0 → 0.1.1-dev.20261021204606

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
@@ -68,7 +68,7 @@ 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);
71
+ "scopes": ["reports:read"] // reports:read = analytics; hawksearch:read = dashboard API (read) and preview tools;
72
72
  // hawksearch:write = create/update/delete synonyms, redirects, banners, …
73
73
  // through the dashboard API (see the bridge's "Writing to HawkSearch");
74
74
  // data:read = your own SQL queries in queries/ (see "Custom data");
@@ -79,6 +79,8 @@ separately, see [Clients](#clients)):
79
79
  // (with "systemParameters", see "HawkSearch system parameters");
80
80
  // hawksearch:search = HawkSearch search and autocomplete, live or preview;
81
81
  // hawksearch:recommendations = the engine's recommendation widgets;
82
+ // hawkai:read = HawkAI agents and smart responses: list, run a smart
83
+ // response, open an agent in HawkAI's assistant widget;
82
84
  // hawksearch:index = rebuild the engine's index (where HawkSearch allows it)
83
85
  // and read its indexes; hawksearch:index:items = add, update, delete items;
84
86
  // hawksearch:index:manage = create, switch and delete indexes, hierarchy, mappings;
@@ -338,14 +340,40 @@ instead of guessing:
338
340
  what fails review. Its project section matches the app's template, or, for an app set up with `init`, says what the
339
341
  project has to do itself.
340
342
  - **`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`.
343
+ - **`.vscode/mcp.json`** (VS Code / Copilot), which starts the CLI's MCP server (see below) from `node_modules`.
344
344
 
345
345
  The CLI owns the part of AGENTS.md between its `<!-- hawk-app:start … -->` and `<!-- hawk-app:end -->` markers, and
346
346
  `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).
347
+ An existing AGENTS.md or CLAUDE.md is never replaced: the CLI's part (or the import) is added to it. An existing
348
+ `.vscode/mcp.json` gets the `hawk-apps` server added (one with comments is left alone: add it yourself).
349
+
350
+ ### Claude Code plugin (`hawk-app claude`)
351
+
352
+ Claude Code gets the MCP server, and skills for the common jobs, from the **Hawk Apps plugin**, installed once per
353
+ machine (not per app):
354
+
355
+ ```bash
356
+ npx @bridgeline-digital/hawk-app-cli@latest claude
357
+ ```
358
+
359
+ It copies the plugin, which ships in this package, next to your login (`%APPDATA%\hawk-app\claude-plugin` on Windows,
360
+ `~/.config/hawk-app/claude-plugin` elsewhere) and adds it to Claude Code as a local marketplace with `claude plugin`
361
+ when that command is on the PATH. Otherwise it prints the two commands to type in the Claude Code panel:
362
+ `/plugin marketplace add <folder>` and `/plugin install hawk-apps@hawk-apps`. No repository access is needed. Run it
363
+ again to update: the plugin carries the CLI's version, so Claude Code takes the new one (`/reload-plugins` in an open
364
+ session). Engineers can also add the repository itself as the marketplace (`.claude-plugin/marketplace.json` at its root).
365
+
366
+ | Skill | What it does |
367
+ | --- | --- |
368
+ | `/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`. |
369
+ | `/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. |
370
+ | `/hawk-apps:add-function` | A function, job or schedule: the declaration, the handler, secrets set by you in a terminal, errors, `validate`. |
371
+ | `/hawk-apps:prepare-review` | The approvers' checks (bundle, dependencies, accessibility, outside origins, scopes, queries, functions): fixes them or says what's left. |
372
+
373
+ Claude also picks a skill on its own when your request matches it. The plugin's MCP server starts in the open folder,
374
+ from the app's own CLI (`node_modules`) when it has one, else the released CLI through `npx`. The CLI no longer writes
375
+ `.mcp.json` for Claude Code; `upgrade` takes the old `hawk-apps` entry out of it (other servers are kept), so the tools
376
+ aren't listed twice.
349
377
 
350
378
  ### MCP server (`hawk-app mcp`)
351
379
 
@@ -369,17 +397,17 @@ Protocol](https://modelcontextprotocol.io)), so they can look things up and chec
369
397
  | `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
398
  | `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
399
 
372
- It also has **prompts**, ready-made tasks that use the tools. Claude Code lists them as slash commands (type `/` and
373
- `hawk`):
400
+ It also has **prompts**, ready-made tasks that use the tools (in Claude Code, the plugin's skills cover the same
401
+ jobs; its slash commands list the prompts too, under the plugin's server):
374
402
 
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. |
403
+ | Prompt | What it does |
404
+ | --- | --- |
405
+ | `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. |
406
+ | `add-feature` | Finds the bridge calls, scopes and showcase examples it needs, says what the review will see, makes the change and validates it. |
407
+ | `explain-app` | What the app does, its scopes (unused or missing ones too) and where it stands in review. |
408
+ | `deploy-to-client` | Deploys to dev and gives a client the app; you pick the client from the matches for its name. |
381
409
 
382
- Each takes an optional `dir` for a workspace with several apps. In VS Code with Copilot they're under `/` too.
410
+ Each takes an optional `dir` for a workspace with several apps. In VS Code with Copilot they're under `/`.
383
411
 
384
412
  - **Dev only.** Every tool runs with `--env dev`; nothing reaches prod. Approvals, secrets, removing clients and logins
385
413
  aren't tools.
@@ -392,9 +420,10 @@ Each takes an optional `dir` for a workspace with several apps. In VS Code with
392
420
  critical vulnerabilities) the tool stops and tells the assistant, which has to ask you and call again with `force`.
393
421
  - **Several apps in one workspace?** Each tool takes `dir`, the app's folder relative to the workspace.
394
422
 
395
- `create` and `init` add it to `.mcp.json` and `.vscode/mcp.json`, started as
423
+ `create` and `init` add it to `.vscode/mcp.json`, started as
396
424
  `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`.
425
+ `validate` warns when it isn't); Claude Code gets it from the plugin (above). For Cursor, add the same command to
426
+ `.cursor/mcp.json` under `mcpServers`.
398
427
 
399
428
  ## Clients
400
429
 
package/agents/AGENTS.md CHANGED
@@ -52,7 +52,9 @@ app.onContextChange(render); // the user switched client, engine or dates
52
52
  | `app.analytics.report('queries', { limit, startDate, endDate, filters })` | `reports:read` |
53
53
  | `app.dashboard.get('Synonym')`: HawkSearch dashboard API, no API version in the path | `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.search(...)`, `app.hawksearch.autocomplete(...)` on the user's engine; preview: `IsInPreview`, `SmartBar`, `ClientData.PreviewBuckets`, `IndexName`, `ExplainDocId` | `hawksearch:search` |
56
+ | `app.hawksearch.preview.settings/indexes/item/explain`: the preview page's visitor targets, indexes, item details, keyword explain | `hawksearch:read` |
57
+ | `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
58
  | `app.data.query('name', params)`: your SQL in `queries/<name>.sql` + `.json` | `data:read` |
57
59
  | `app.functions.call('name', input)`, `.start/.wait/.job` for jobs: `functions/<name>.ts` exporting `handler` | `functions:invoke` |
58
60
  | `app.email.send(...)` | `email:send` |
@@ -92,8 +94,10 @@ https://www.npmjs.com/package/@bridgeline-digital/hawk-app-bridge. Its TypeScrip
92
94
 
93
95
  ## MCP server
94
96
 
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
97
+ The CLI's MCP server, `hawk-apps`: Claude Code gets it from the Hawk Apps plugin (`npx @bridgeline-digital/hawk-app-cli@latest
98
+ claude`, once per machine; it also adds the skills `/hawk-apps:new-screen`, `/hawk-apps:add-query`,
99
+ `/hawk-apps:add-function` and `/hawk-apps:prepare-review`), and VS Code from `.vscode/mcp.json`; both start this app's
100
+ copy in `node_modules` (`npm install` first). Its tools: `docs` (the bridge, UI kit, CLI, `hawk-app.json` schema and the
97
101
  showcase app, which has working code for every feature), `ui_components` (the UI kit's components for this app's
98
102
  template: use them before writing your own), `validate`, `status` (with the review checks approvers will see),
99
103
  `analytics_reports` (every report `app.analytics.report()` can call, with its columns and filters),
@@ -106,7 +110,7 @@ isn't approved, only Bridgeline staff in it see the app. Only approvers change w
106
110
  gets. `hawk-app deploy out --alias staging` deploys off the main line: the version carries this hawk-app.json, `staging`
107
111
  points at it, and the version under review isn't touched. A plain `hawk-app deploy` sends hawk-app.json first, so scope
108
112
  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.
113
+ `explain-app`, `deploy-to-client`) are ready-made tasks that use them; in Claude Code prefer the plugin's skills.
110
114
 
111
115
  ## Icon
112
116
 
@@ -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,34 @@
1
+ ---
2
+ name: add-function
3
+ description: Add or change a server-side function (functions/<name>.ts, AWS Lambda), a background job or a schedule in a Hawk App. Use when the app needs server code, a third-party API with a secret key, work longer than a page should wait, or something on a timer.
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)`.
31
+ 5. **Call it** from the app with `app.functions.call('<name>', input)` and handle `function_failed`,
32
+ `function_timeout`, `secret_missing` and `functions_not_enabled`.
33
+ 6. **Check it.** Run `validate`. Functions run only from a **deployed** version; deploy only when the user asks. After
34
+ 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,32 @@
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
+ 6. **Check it.** Run the hawk-apps `validate` tool (with `build: true` when the build setup changed) and fix what it
32
+ says. Offer `npm run app:dev` to see it locally. Deploy only when the user asks.
@@ -0,0 +1,32 @@
1
+ ---
2
+ name: prepare-review
3
+ description: Get a Hawk App ready to submit for approval in HawkSearch - run the approvers' automated checks (bundle, dependencies, accessibility, outside origins, queries, functions, scopes), fix what fails and say what's left. Use before submitting, after a big change, or when the user asks whether the app will pass review.
4
+ ---
5
+
6
+ # Prepare for review
7
+
8
+ Approvers see automated checks in HawkSearch's approve dialog; a failed one forces them to override it. Get every check
9
+ green, or explain to the user why one can't be.
10
+
11
+ 1. **Validate with the build.** Run the hawk-apps `validate` tool with `build: true`. Fix what it reports in the code;
12
+ ask the user before changing `scopes` or `externalOrigins` in `hawk-app.json`.
13
+ 2. **The review checklist** (fix or explain each):
14
+ - **Bundle:** under 20 MB in total, no file over 5 MB, not 50% (and 1 MB) bigger than the released version.
15
+ - **Dependencies:** no high or critical findings in `npm run app:audit` (production dependencies).
16
+ - **Accessibility:** no critical axe issues (`validate --build` lists them): labels, `alt`, one `<main>`, headings
17
+ in order.
18
+ - **Outside origins:** `externalOrigins` lists each https origin the app calls or loads from; `["*"]` fails
19
+ (`"img:*"` is fine for images).
20
+ - **Scopes:** each one is used by the code, and every bridge call's scope is there (`validate` warns about missing
21
+ ones). Fewer is better: elevated scopes (`data:read`, `functions:invoke`, `hawksearch:write`, `ai:invoke`, …) are
22
+ never auto-approved.
23
+ - **Queries:** each has `{{partitions(...)}}`, names its columns and has a `LIMIT` on lists.
24
+ - **Functions and external Lambdas:** shown to the approver with their code and settings; Engineering must have
25
+ enabled functions.
26
+ - **No secrets** anywhere in the app or the build.
27
+ 3. **What approvers will see.** Run `status`: the review checks on the newest version, and `deploy` with
28
+ `dryRun: true`: what `hawk-app.json` would change. If the newest version is older than the code, say so: the user
29
+ decides when to deploy.
30
+ 4. **Errors.** Run `errors` and fix what the deployed versions threw, the most frequent first.
31
+ 5. **Finish** with a short list: ready to submit (open the app with `npx hawk-app open` and click **Submit for
32
+ approval**), or what's left. Never deploy to prod or submit for the user.
@@ -265,7 +265,8 @@ Any resource of the [HawkSearch Dashboard API](https://developerdocs.hawksearch.
265
265
  `Synonym`, `LandingPage`, `Field`, …. Leave the API version out; the platform picks it. Responses are HawkSearch's
266
266
  body as is. To change data, see [Writing to HawkSearch](#writing-to-hawksearch-hawksearchwrite). For reporting, use
267
267
  analytics. Your assistant can look up any operation's fields, its bridge call and scope with the `hawk-apps` MCP
268
- server's `hawksearch_api` tool.
268
+ server's `hawksearch_api` tool. The same scope gives the preview page's tools (`app.hawksearch.preview`: visitor targets,
269
+ indexes, item details, keyword explain): see [Preview like HawkSearch's preview page](#preview-like-hawksearchs-preview-page).
269
270
 
270
271
  ### Writing to HawkSearch: `hawksearch:write`
271
272
 
@@ -489,7 +490,7 @@ what `handler` returned (up to 6 MB). It waits up to `timeoutMs` (default 30 s)
489
490
  | `ctx.app.slug`, `ctx.version` | Your app and the version the caller has open (the function code is that version's). |
490
491
  | `ctx.secrets.NAME` | The secrets the function declares in `hawk-app.json` (see below). Nothing else. |
491
492
  | `ctx.config` | The app's [config](#app-config) for the caller's client and this version, already resolved: `{ key: value }` for every declared key (null when nothing is set and there's no default). Frozen. |
492
- | `ctx.platform` | The platform API **as the caller**: `analytics.report`, `dashboard.get/post/put/delete`, `data.query` (polled for you), [`email.send`](#email-emailsend), [`hawksearch.search/autocomplete`](#hawksearch-search-hawksearchsearch), [`hawksearch.systemParameters`](#hawksearch-system-parameters-hawksearchsysparamswrite), [`functions.start/job/wait`](#background-jobs), [`ai.generate/stream/info`](#ai-aiinvoke) and `api.get/post`, with your app's scopes. Analytics dates don't default on the server: pass them. |
493
+ | `ctx.platform` | The platform API **as the caller**: `analytics.report`, `dashboard.get/post/put/delete`, `data.query` (polled for you), [`email.send`](#email-emailsend), [`hawksearch.search/autocomplete`](#hawksearch-search-hawksearchsearch), [`hawksearch.preview`](#preview-like-hawksearchs-preview-page), [`hawkai`](#hawkai-agents-and-smart-responses-hawkairead), [`hawksearch.systemParameters`](#hawksearch-system-parameters-hawksearchsysparamswrite), [`functions.start/job/wait`](#background-jobs), [`ai.generate/stream/info`](#ai-aiinvoke) and `api.get/post`, with your app's scopes. Analytics dates don't default on the server: pass them. |
493
494
  | `ctx.job`, `ctx.trigger` | For a [background job](#background-jobs): `{ id, progress(percent?, message?) }` (null for a call), and what started the run (`call` or `schedule`). |
494
495
  | `ctx.aws` | Only with `data:read`: short-lived credentials scoped to this client's analytics data, plus `region`, `database`, `workGroup`, `outputLocation`, for the AWS SDK's Athena client. Prefer `ctx.platform.data.query` when an app query does the job. |
495
496
 
@@ -713,6 +714,71 @@ await app.hawksearch.autocomplete({ Keyword: 'boo', ProductCount: 5 });
713
714
 
714
715
  Other HawkSearch errors (e.g. `400` for an unknown `IndexName`) come back as HawkSearch sent them.
715
716
 
717
+ ### Preview like HawkSearch's preview page
718
+
719
+ A preview search takes everything the preview page sets: `IsInPreview: true`, `SmartBar` (`PreviewDate` and switches
720
+ such as `BoostAndBury: false`, `ItemPin: false`), `ClientData: { PreviewBuckets: [ids] }` for visitor targets,
721
+ `IndexName`, `Is100CoverageTurnedOn`, and `ExplainDocId` to explain one result's score (`DocExplain` in the answer). The page's own pickers and explain panel are `app.hawksearch.preview` (scope **`hawksearch:read`**,
722
+ within the same per-minute limit):
723
+
724
+ ```ts
725
+ const settings = await app.hawksearch.preview.settings(); // visitor targets, fields, layouts
726
+ const { ClientIndexes, CurrentIndex } = await app.hawksearch.preview.indexes();
727
+ const item = await app.hawksearch.preview.item('SKU-123'); // one item as indexed ({ indexName } optional)
728
+ const why = await app.hawksearch.preview.explain({ Keyword: 'jackets', IsInPreview: true });
729
+ why.AdjustedKeyword; why.AppliedKeywordReplacementRules; why.ElasticQuery;
730
+ ```
731
+
732
+ `explain` needs the session's site API key (`403 preview_needs_site_key` with a group key). Pinning from an app isn't
733
+ available yet.
734
+
735
+ ## HawkAI agents and smart responses: `hawkai:read`
736
+
737
+ The client's HawkAI agents and smart responses for the user's current engine, as Spark lists them: names and settings,
738
+ never prompts. From the app or a function (`ctx.platform.hawkai`).
739
+
740
+ ```ts
741
+ const agents = await app.hawkai.agents(); // by name
742
+ const responses = await app.hawkai.smartResponses(); // with maxItems and allowRequestOverride
743
+ const one = await app.hawkai.smartResponse(responses[0].id);
744
+ ```
745
+
746
+ Each is `{ id, name, description, model, allEngines, maxItems, allowRequestOverride }` (`allEngines`: set up for every
747
+ engine of the client).
748
+
749
+ **Run a smart response** for a query and a search, live or in preview, as the storefront does. The platform adds the
750
+ client, the engine's ClientGuid and its search API; HawkAI runs the search and picks the items:
751
+
752
+ ```ts
753
+ const answer = await app.hawkai.runSmartResponse(id, {
754
+ query: 'waterproof jacket',
755
+ search: { Keyword: 'jacket', IsInPreview: true, SmartBar: { PreviewDate: '2026-11-27T00:00:00' } },
756
+ });
757
+ answer.recommendation; // Markdown
758
+ answer.Results; // [{ DocId, Document, reason }]
759
+ ```
760
+
761
+ Answered whole (not streamed), within the search limit. **An assistant** runs in HawkAI's widget
762
+ ([`@bridgeline-digital/hawkai-assistant`](https://www.npmjs.com/package/@bridgeline-digital/hawkai-assistant)), which
763
+ talks to HawkAI from the browser: `app.hawkai.assistant(agentId)` gives it `{ apiUrl, accountId, agentId }`.
764
+
765
+ ```ts
766
+ HawkAI.Assistant.initialize({ ...(await app.hawkai.assistant(agentId)), ui: { display: 'embedded', embedHostSelector: '#assistant' } });
767
+ ```
768
+
769
+ List HawkAI's origin in `externalOrigins` (`https://assistant-dev.hawksearch.net` on dev, `https://assistant.hawksearch.net`
770
+ on prod). The widget initializes once per page: reload to switch agents.
771
+
772
+ | `error.code` | Status | Meaning |
773
+ |---|---|---|
774
+ | `scope_missing` | 403 | Add `hawkai:read` to `scopes`. |
775
+ | `engine_required` | 400 | The host session has no engine. |
776
+ | `hawkai_not_found` | 404 | No agent / smart response with that id for this client and engine. |
777
+ | `hawkai_unavailable` | 502 | HawkAI's configuration couldn't be read, or HawkAI didn't answer in time: try again. |
778
+ | `hawkai_error` | 502 | HawkAI answered without a smart response (its reason is in the message). |
779
+ | `hawkai_limit_reached` | 429 | HawkAI's own limit for the client (tokens or rate). |
780
+ | `too_many_requests` | 429 | Over the search limit (smart responses count as searches); wait `retryAfterMs`. |
781
+
716
782
  ## HawkSearch system parameters: `hawksearch:sysparams:write`
717
783
 
718
784
  Let users change a few HawkSearch **system parameters** of the engine they are working on (Sites → System Parameters
@@ -1264,7 +1330,7 @@ Which clients have the app is not in this file: manage them in the Hawk Analytic
1264
1330
  | `hosting` | `static` (the default): built files on the apps CDN. `server` is reserved for server-rendered apps and not accepted yet. |
1265
1331
  | `visibility` | `allowList` (only the clients given the app), `all` (every client except excluded ones), or `internal` (Bridgeline staff). The clients themselves are managed in the Hawk Analytics portal or with `npx hawk-app clients add`/`exclude`/`remove`; a `hawk-app.json` with `clients` or `excludeClients` is refused. |
1266
1332
  | `listed` | `true` (the default) shows the app on the host's Apps page. `false` leaves it off that list, and the host hides its own title and back button for it, but it still opens by its direct URL (e.g. linked from another app or page). It doesn't change who can open it: `visibility` and the app's clients still apply. |
1267
- | `scopes` | `reports:read` for analytics, `hawksearch:read` for the dashboard API, `hawksearch:write` to [write to it](#writing-to-hawksearch-hawksearchwrite), `data:read` for [custom queries](#custom-data-queries-dataread), `functions:invoke` for [functions](#functions-functionsinvoke), `email:send` for [email](#email-emailsend), `hawksearch:sysparams:write` for [system parameters](#hawksearch-system-parameters-hawksearchsysparamswrite), `hawksearch:search` for [search](#hawksearch-search-hawksearchsearch), `hawksearch:features:write` for [features](#hawksearch-features-hawksearchfeatureswrite), `hawksearch:index` for [index rebuilds](#hawksearch-index-rebuilds-hawksearchindex), `hawksearch:index:items` and `hawksearch:index:manage` for [the Indexing API](#the-indexing-api-hawksearchindex-indexitems-indexmanage), `hawksearch:recommendations` for [recommendations](#hawksearch-recommendations-hawksearchrecommendations), `storage` for [storage](#storage-storage), `lambdas:invoke` for [external Lambdas](#external-lambdas-lambdasinvoke), `ai:invoke` for [AI](#ai-aiinvoke), `assistant` to [ask the host's assistant](#ask-the-hosts-assistant-assistant). |
1333
+ | `scopes` | `reports:read` for analytics, `hawksearch:read` for the dashboard API, `hawksearch:write` to [write to it](#writing-to-hawksearch-hawksearchwrite), `data:read` for [custom queries](#custom-data-queries-dataread), `functions:invoke` for [functions](#functions-functionsinvoke), `email:send` for [email](#email-emailsend), `hawksearch:sysparams:write` for [system parameters](#hawksearch-system-parameters-hawksearchsysparamswrite), `hawksearch:search` for [search](#hawksearch-search-hawksearchsearch), `hawksearch:features:write` for [features](#hawksearch-features-hawksearchfeatureswrite), `hawksearch:index` for [index rebuilds](#hawksearch-index-rebuilds-hawksearchindex), `hawksearch:index:items` and `hawksearch:index:manage` for [the Indexing API](#the-indexing-api-hawksearchindex-indexitems-indexmanage), `hawksearch:recommendations` for [recommendations](#hawksearch-recommendations-hawksearchrecommendations), `hawkai:read` for [HawkAI](#hawkai-agents-and-smart-responses-hawkairead), `storage` for [storage](#storage-storage), `lambdas:invoke` for [external Lambdas](#external-lambdas-lambdasinvoke), `ai:invoke` for [AI](#ai-aiinvoke), `assistant` to [ask the host's assistant](#ask-the-hosts-assistant-assistant). |
1268
1334
  | `functions` | Your [functions](#functions-functionsinvoke) and their settings: `{ "<name>": { "job", "timeoutSeconds", "memoryMb", "secrets" } }`. Recorded with each version. |
1269
1335
  | `schedules` | [Schedules](#schedules) for job functions: `{ "<name>": { "function", "cron", "timezone" } }`, at most 5, at most hourly. Needs `functions:invoke`. Reviewed like scopes. |
1270
1336
  | `systemParameters` | The HawkSearch [system parameters](#hawksearch-system-parameters-hawksearchsysparamswrite) the app may read and set, e.g. `["Timezone"]`. Needs `hawksearch:sysparams:write`. Reviewed like scopes. |
@@ -3,7 +3,7 @@
3
3
  "slug": "showcase",
4
4
  "name": "Hawk Apps Showcase",
5
5
  "icon": "icon.svg",
6
- "description": "Every Hawk Apps feature in one app: context, analytics, dashboard API, search, queries, functions, jobs, system parameters, storage, app config, AI and host messages.",
6
+ "description": "Every Hawk Apps feature in one app: context, analytics, dashboard API, search, preview tools, HawkAI, queries, functions, jobs, system parameters, storage, app config, AI and host messages.",
7
7
  "category": "Showcase",
8
8
  "version": "0.1.0",
9
9
  "visibility": "internal",
@@ -21,7 +21,8 @@
21
21
  "hawksearch:index",
22
22
  "hawksearch:index:items",
23
23
  "ai:invoke",
24
- "storage"
24
+ "storage",
25
+ "hawkai:read"
25
26
  ],
26
27
  "systemParameters": [
27
28
  "Timezone",
@@ -41,7 +42,11 @@
41
42
  "label": "Notification recipients"
42
43
  }
43
44
  },
44
- "externalOrigins": [],
45
+ "externalOrigins": [
46
+ "https://cdn.jsdelivr.net",
47
+ "https://assistant-dev.hawksearch.net",
48
+ "https://assistant.hawksearch.net"
49
+ ],
45
50
  "functions": {
46
51
  "echo": {
47
52
  "timeoutSeconds": 10,
@@ -6,6 +6,7 @@ import {
6
6
  type AiResult,
7
7
  type AppSchedule,
8
8
  type FunctionJob,
9
+ type HawkAiConfig,
9
10
  type HawkApp,
10
11
  type HawkAppContext,
11
12
  type IndexItemsResult,
@@ -24,7 +25,7 @@ import { useHawkApp } from './useHawkApp';
24
25
  * Internal test app for the Hawk Apps platform. One tab per feature; every call shows its outcome, timing and error
25
26
  * code, so a failing platform change is easy to spot. Not a product: keep it small and keep it working.
26
27
  */
27
- const TABS = ['Context', 'Analytics', 'Dashboard', 'Search', 'Recommendations', 'Index', 'Storage', 'Config', 'Queries', 'Functions', 'Jobs', 'System parameters', 'AI', 'Host'] as const;
28
+ const TABS = ['Context', 'Analytics', 'Dashboard', 'Search', 'Preview', 'HawkAI', 'Recommendations', 'Index', 'Storage', 'Config', 'Queries', 'Functions', 'Jobs', 'System parameters', 'AI', 'Host'] as const;
28
29
  type Tab = (typeof TABS)[number];
29
30
 
30
31
  export function App() {
@@ -48,6 +49,8 @@ export function App() {
48
49
  {tab === 'Analytics' && <AnalyticsTab app={hawk.app} context={hawk.context} />}
49
50
  {tab === 'Dashboard' && <DashboardTab app={hawk.app} />}
50
51
  {tab === 'Search' && <SearchTab app={hawk.app} context={hawk.context} />}
52
+ {tab === 'Preview' && <PreviewTab app={hawk.app} />}
53
+ {tab === 'HawkAI' && <HawkAiTab app={hawk.app} />}
51
54
  {tab === 'Recommendations' && <RecommendationsTab app={hawk.app} />}
52
55
  {tab === 'Index' && <IndexTab app={hawk.app} />}
53
56
  {tab === 'Storage' && <StorageTab app={hawk.app} />}
@@ -1408,6 +1411,237 @@ function RecommendationsTab({ app }: { app: HawkApp }) {
1408
1411
  );
1409
1412
  }
1410
1413
 
1414
+ /** The preview page's tools (hawksearch:read) and a preview search with what they return (hawksearch:search). */
1415
+ function PreviewTab({ app }: { app: HawkApp }) {
1416
+ const [outcomes, setOutcomes] = useState<Outcome[]>([]);
1417
+ const [busy, setBusy] = useState(false);
1418
+ const [keyword, setKeyword] = useState('jacket');
1419
+ const [itemId, setItemId] = useState('');
1420
+ const [buckets, setBuckets] = useState('');
1421
+ const [indexName, setIndexName] = useState('');
1422
+
1423
+ const go = async (runs: () => Promise<Outcome[]>) => {
1424
+ setBusy(true);
1425
+ setOutcomes(await runs());
1426
+ setBusy(false);
1427
+ };
1428
+
1429
+ const tools = async () => [
1430
+ await measure('hawksearch.preview.settings()', async () => {
1431
+ const settings = await app.hawksearch.preview.settings();
1432
+ return { summary: `fields: ${Object.keys(settings).join(', ') || 'none'}`, detail: settings };
1433
+ }),
1434
+ await measure('hawksearch.preview.indexes()', async () => {
1435
+ const indexes = await app.hawksearch.preview.indexes();
1436
+ if (typeof indexes.CurrentIndex === 'string' && !indexName) setIndexName(indexes.CurrentIndex);
1437
+ return { summary: `current: ${String(indexes.CurrentIndex ?? 'none')}`, detail: indexes };
1438
+ }),
1439
+ await measure(`hawksearch.preview.explain({ Keyword: ${JSON.stringify(keyword)}, IsInPreview: true })`, async () => {
1440
+ const why = await app.hawksearch.preview.explain({ Keyword: keyword, IsInPreview: true });
1441
+ return { summary: `adjusted keyword: ${why.AdjustedKeyword ?? keyword}`, detail: why };
1442
+ }),
1443
+ ];
1444
+
1445
+ // As the preview page searches: preview rules, the chosen visitor targets and index, and one result explained.
1446
+ const previewSearch = () => {
1447
+ const previewBuckets = buckets.split(',').map((b) => Number(b.trim())).filter((b) => Number.isInteger(b) && b > 0);
1448
+ const request: SearchRequest = {
1449
+ Keyword: keyword,
1450
+ IsInPreview: true,
1451
+ ...(indexName ? { IndexName: indexName } : {}),
1452
+ ...(previewBuckets.length ? { ClientData: { PreviewBuckets: previewBuckets } } : {}),
1453
+ ...(itemId.trim() ? { ExplainDocId: itemId.trim() } : {}),
1454
+ };
1455
+ return measure(`hawksearch.search(${JSON.stringify(request)})`, async () => searchSummary(await app.hawksearch.search(request)));
1456
+ };
1457
+
1458
+ return (
1459
+ <Card title="Preview (hawksearch:read, hawksearch:search)">
1460
+ <p className="muted">
1461
+ The preview page's tools for this engine, and a preview search with what they give: visitor targets (ids from the settings),
1462
+ the index, and one result's explain.
1463
+ </p>
1464
+ <div className="buttons">
1465
+ <label>
1466
+ Keyword <input value={keyword} onChange={(e) => setKeyword(e.target.value)} />
1467
+ </label>
1468
+ <label>
1469
+ Visitor target ids <input value={buckets} onChange={(e) => setBuckets(e.target.value)} placeholder="e.g. 12, 14" />
1470
+ </label>
1471
+ <label>
1472
+ Index <input value={indexName} onChange={(e) => setIndexName(e.target.value)} placeholder="blank: current" />
1473
+ </label>
1474
+ <label>
1475
+ Item id <input value={itemId} onChange={(e) => setItemId(e.target.value)} placeholder="item details, explain" />
1476
+ </label>
1477
+ </div>
1478
+ <div className="buttons">
1479
+ <button disabled={busy} onClick={() => go(tools)}>
1480
+ Settings, indexes, keyword explain
1481
+ </button>
1482
+ <button
1483
+ disabled={busy || !itemId.trim()}
1484
+ onClick={() =>
1485
+ go(async () => [
1486
+ await measure(`hawksearch.preview.item(${JSON.stringify(itemId.trim())})`, async () => {
1487
+ const item = await app.hawksearch.preview.item(itemId.trim(), indexName ? { indexName } : {});
1488
+ return { summary: 'found', detail: item };
1489
+ }),
1490
+ ])
1491
+ }
1492
+ >
1493
+ Item details
1494
+ </button>
1495
+ <button disabled={busy} onClick={() => go(async () => [await previewSearch()])}>
1496
+ Preview search
1497
+ </button>
1498
+ </div>
1499
+ <Results outcomes={outcomes} />
1500
+ </Card>
1501
+ );
1502
+ }
1503
+
1504
+ /** Where HawkAI's assistant widget (@bridgeline-digital/hawkai-assistant) comes from: its one-file build, pinned. */
1505
+ const HAWKAI_ASSISTANT_SCRIPT = 'https://cdn.jsdelivr.net/npm/@bridgeline-digital/hawkai-assistant@1.1.0/dist/hawkai-assistant.js';
1506
+
1507
+ declare global {
1508
+ interface Window {
1509
+ HawkAI?: { Assistant?: { initialize(config: Record<string, unknown>): Promise<void> } };
1510
+ }
1511
+ }
1512
+
1513
+ /** Loads the widget's script once. */
1514
+ function loadAssistantScript(): Promise<void> {
1515
+ if (window.HawkAI?.Assistant) return Promise.resolve();
1516
+ return new Promise((resolve, reject) => {
1517
+ const script = document.createElement('script');
1518
+ script.src = HAWKAI_ASSISTANT_SCRIPT;
1519
+ script.onload = () => resolve();
1520
+ script.onerror = () => reject(new Error(`could not load ${HAWKAI_ASSISTANT_SCRIPT} (is it in externalOrigins?)`));
1521
+ document.head.appendChild(script);
1522
+ });
1523
+ }
1524
+
1525
+ /** HawkAI (hawkai:read): pick a smart response and run it on a preview search; pick an agent and open it in HawkAI's widget. */
1526
+ function HawkAiTab({ app }: { app: HawkApp }) {
1527
+ const [outcomes, setOutcomes] = useState<Outcome[]>([]);
1528
+ const [busy, setBusy] = useState(false);
1529
+ const [smartResponses, setSmartResponses] = useState<HawkAiConfig[]>([]);
1530
+ const [agents, setAgents] = useState<HawkAiConfig[]>([]);
1531
+ const [smartResponseId, setSmartResponseId] = useState('');
1532
+ const [agentId, setAgentId] = useState('');
1533
+ const [query, setQuery] = useState('a warm waterproof jacket');
1534
+ const [preview, setPreview] = useState(true);
1535
+ const [answer, setAnswer] = useState('');
1536
+ // The widget initializes once per page: the agent it was opened with.
1537
+ const [openedAgent, setOpenedAgent] = useState('');
1538
+
1539
+ const go = async (runs: () => Promise<Outcome[]>) => {
1540
+ setBusy(true);
1541
+ setOutcomes(await runs());
1542
+ setBusy(false);
1543
+ };
1544
+
1545
+ const load = async () => [
1546
+ await measure('hawkai.smartResponses()', async () => {
1547
+ const list = await app.hawkai.smartResponses();
1548
+ setSmartResponses(list);
1549
+ setSmartResponseId((current) => current || list[0]?.id || '');
1550
+ return { summary: `${list.length} smart responses`, detail: list };
1551
+ }),
1552
+ await measure('hawkai.agents()', async () => {
1553
+ const list = await app.hawkai.agents();
1554
+ setAgents(list);
1555
+ setAgentId((current) => current || list[0]?.id || '');
1556
+ return { summary: `${list.length} agents`, detail: list };
1557
+ }),
1558
+ ];
1559
+
1560
+ const run = async () => [
1561
+ await measure(`hawkai.runSmartResponse(${JSON.stringify(smartResponseId)}, { query, search${preview ? ': IsInPreview' : ''} })`, async () => {
1562
+ setAnswer('');
1563
+ const result = await app.hawkai.runSmartResponse(smartResponseId, { query, search: { Keyword: query, IsInPreview: preview } });
1564
+ setAnswer(result.recommendation ?? '');
1565
+ return { summary: `${result.Results?.length ?? 0} items, ${result.usage?.totalTokens ?? '?'} tokens`, detail: result };
1566
+ }),
1567
+ ];
1568
+
1569
+ const openAssistant = async () => [
1570
+ await measure(`hawkai.assistant(${JSON.stringify(agentId)}) + HawkAI.Assistant.initialize`, async () => {
1571
+ const config = await app.hawkai.assistant(agentId);
1572
+ await loadAssistantScript();
1573
+ await window.HawkAI!.Assistant!.initialize({ ...config, ui: { display: 'embedded', embedHostSelector: '#showcase-assistant', open: true } });
1574
+ setOpenedAgent(agentId);
1575
+ return { summary: `${config.apiUrl}, account ${config.accountId}`, detail: config };
1576
+ }),
1577
+ ];
1578
+
1579
+ const name = (list: HawkAiConfig[], id: string) => list.find((c) => c.id === id)?.name ?? id;
1580
+
1581
+ return (
1582
+ <>
1583
+ <Card title="Smart response (hawkai:read)">
1584
+ <p className="muted">
1585
+ Pick one of the client's smart responses for this engine and run it: HawkAI runs the search itself (the preview search API with
1586
+ IsInPreview); the platform adds the client, ClientGuid and search API.
1587
+ </p>
1588
+ <div className="buttons">
1589
+ <button disabled={busy} onClick={() => go(load)}>
1590
+ Load smart responses and agents
1591
+ </button>
1592
+ </div>
1593
+ <div className="buttons">
1594
+ <label>
1595
+ Smart response{' '}
1596
+ <select value={smartResponseId} onChange={(e) => setSmartResponseId(e.target.value)}>
1597
+ {smartResponses.map((c) => (
1598
+ <option key={c.id} value={c.id}>
1599
+ {c.name}
1600
+ {c.allEngines ? ' (all engines)' : ''}
1601
+ </option>
1602
+ ))}
1603
+ </select>
1604
+ </label>
1605
+ <label>
1606
+ Query <input value={query} onChange={(e) => setQuery(e.target.value)} />
1607
+ </label>
1608
+ <label>
1609
+ <input type="checkbox" checked={preview} onChange={(e) => setPreview(e.target.checked)} /> Preview
1610
+ </label>
1611
+ <button disabled={busy || !smartResponseId || !query.trim()} onClick={() => go(run)}>
1612
+ Run
1613
+ </button>
1614
+ </div>
1615
+ {answer && <pre>{answer}</pre>}
1616
+ </Card>
1617
+ <Card title="Assistant (hawkai:read + HawkAI's widget)">
1618
+ <p className="muted">
1619
+ Opens an agent in HawkAI's assistant widget, which talks to HawkAI from the browser (its origin is in externalOrigins). The widget
1620
+ starts once per page: reload the app to switch agents.
1621
+ </p>
1622
+ <div className="buttons">
1623
+ <label>
1624
+ Agent{' '}
1625
+ <select value={agentId} onChange={(e) => setAgentId(e.target.value)} disabled={!!openedAgent}>
1626
+ {agents.map((c) => (
1627
+ <option key={c.id} value={c.id}>
1628
+ {c.name}
1629
+ </option>
1630
+ ))}
1631
+ </select>
1632
+ </label>
1633
+ <button disabled={busy || !agentId || !!openedAgent} onClick={() => go(openAssistant)}>
1634
+ Open assistant
1635
+ </button>
1636
+ {openedAgent && <span className="muted">Open: {name(agents, openedAgent)}. Reload to switch.</span>}
1637
+ </div>
1638
+ <div id="showcase-assistant" style={{ height: openedAgent ? 520 : 0 }} />
1639
+ </Card>
1640
+ <Results outcomes={outcomes} />
1641
+ </>
1642
+ );
1643
+ }
1644
+
1411
1645
  function HostTab({ app }: { app: HawkApp }) {
1412
1646
  const [outcomes, setOutcomes] = useState<Outcome[]>([]);
1413
1647
  const [tall, setTall] = useState(false);
package/docs/ui/README.md CHANGED
@@ -91,9 +91,9 @@ see it in a real one). `<HawkAppProvider preview={{ mode: 'dark' }}>` renders wi
91
91
  | Part | From |
92
92
  |---|---|
93
93
  | Typography (Montserrat, 14 px base, h1–h3, `featuredValue`), breakpoints (`sm` 640 … `2xl` 1536), spacing, card and list overrides | Spark's theme (`sparkThemeOptions`) |
94
- | Primary color and its text, backgrounds, text colors, dividers, border radius, light or dark | The host's theme tokens (`color-button-accent-background`, `color-background`, `color-background-container`, `color-text-default`, `color-text-secondary`, `color-border-default`, `border-radius`, `mode`), else Spark's |
94
+ | Primary color and its text, backgrounds, text colors, dividers, border radius, light or dark | The host's theme tokens (`color-button-accent-background`, `color-background`, `color-background-container`, `color-text-default`, `color-text-secondary`, `color-border-default`, `border-radius`, `mode`), else Spark's. A `color-border-default` too faint to see on the host's surface (contrast under 1.2, such as HawkSearch's `#f5f7fa` on white) is left out, so dividers, table rows and outlined cards keep MUI's divider |
95
95
  | Dialogs and snackbars in the part of the frame the user can see | The kit (`frameThemeOptions`, from the bridge's `--hawk-visible-*` variables) |
96
- | Table rows and header (borders in the divider color, a header row on the page background) | The kit (`tableThemeOptions`) |
96
+ | Table rows and header (borders in the divider color, a header row on the page background, rows 10px above and below, every other body row tinted; the stylesheet's `.hawk-table` alike) | The kit (`tableThemeOptions`) |
97
97
 
98
98
  The font is always Spark's, whatever the host uses, so apps look the same in HawkSearch and Spark.
99
99
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bridgeline-digital/hawk-app-cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.1-dev.20261021204606",
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",
@@ -17,6 +17,7 @@
17
17
  "files": [
18
18
  "agents",
19
19
  "bin",
20
+ "claude-plugin",
20
21
  "docs",
21
22
  "src",
22
23
  "schema",
@@ -99,7 +99,7 @@
99
99
  "default": "static"
100
100
  },
101
101
  "scopes": {
102
- "description": "Data the app needs. reports:read (HawkSearch Analytics reports), hawksearch:read (read the HawkSearch dashboard API), hawksearch:write (create, update and delete in the HawkSearch dashboard API: banners, boost/bury, campaigns, visibility, visitor targets, redirects, synonyms, spellcheck, pagination and sorting sets, recommendation visibility, landing page layouts, facets and landing pages; site API keys only, limited per app and client per minute, every write audit-logged), data:read (the app's own SQL queries in queries/, run against the current client's analytics data), functions:invoke (the app's own server-side functions in functions/, see \"functions\"), email:send (send email from noreply@hawksearch.com with the app's name, from the app or its functions; sends are limited per app and client per day), hawksearch:sysparams:write (read and set the HawkSearch system parameters listed in \"systemParameters\" for the user's current engine), hawksearch:search (HawkSearch search and autocomplete for the user's current engine, live or in preview; limited per app and client per minute), ai:invoke (Claude through the platform, tiers fast and smart, from the app or its functions; a daily token budget per tier and requests per minute per app and client; an app with it is never auto-approved), lambdas:invoke (call the Lambdas outside Hawk Apps listed in \"lambdas\" that the platform allows, with app.lambdas.invoke; an app with it is never auto-approved) and hawksearch:features:write (turn HawkSearch features on and off for the user's current engine, additional and facet features, one code at a time with app.hawksearch.features; any feature, including paid add-ons, so an app with it is never auto-approved), hawksearch:index (rebuild the user's engine's index from its feed and follow the rebuild with app.hawksearch.index, only where the engine's EnableRebuildIndex system parameter is on, one rebuild at a time per engine, and read the engine's indexes, current index, hierarchy and mappings through HawkSearch's Indexing API; never auto-approved), hawksearch:index:items (add, update and delete items and their attributes in the engine's indexes through the Indexing API, live at once on the current index; at most 125 items a call; never auto-approved), hawksearch:index:manage (create indexes, set the current one, delete others, rebuild autocomplete and the rest, hierarchy and facet mappings through the Indexing API; never auto-approved), hawksearch:recommendations (HawkSearch's recommendation widgets for the user's current engine with app.hawksearch.recommendations, never tracked in the client's recommendation reports; limited per app and client per minute), storage (the app's own key-value data and files kept by the platform with app.storage and app.storage.files, per app and client, within quotas; shared by everyone at the client and the app's functions and schedules) and assistant (ask the host's assistant, Hawk Assistant in HawkSearch, as the user with app.assistant.ask, from a click; the user sees the question and the answer in the assistant's chat; show the buttons only while context.features.assistant is true).",
102
+ "description": "Data the app needs. reports:read (HawkSearch Analytics reports), hawksearch:read (read the HawkSearch dashboard API), hawksearch:write (create, update and delete in the HawkSearch dashboard API: banners, boost/bury, campaigns, visibility, visitor targets, redirects, synonyms, spellcheck, pagination and sorting sets, recommendation visibility, landing page layouts, facets and landing pages; site API keys only, limited per app and client per minute, every write audit-logged), data:read (the app's own SQL queries in queries/, run against the current client's analytics data), functions:invoke (the app's own server-side functions in functions/, see \"functions\"), email:send (send email from noreply@hawksearch.com with the app's name, from the app or its functions; sends are limited per app and client per day), hawksearch:sysparams:write (read and set the HawkSearch system parameters listed in \"systemParameters\" for the user's current engine), hawksearch:search (HawkSearch search and autocomplete for the user's current engine, live or in preview; limited per app and client per minute), ai:invoke (Claude through the platform, tiers fast and smart, from the app or its functions; a daily token budget per tier and requests per minute per app and client; an app with it is never auto-approved), lambdas:invoke (call the Lambdas outside Hawk Apps listed in \"lambdas\" that the platform allows, with app.lambdas.invoke; an app with it is never auto-approved) and hawksearch:features:write (turn HawkSearch features on and off for the user's current engine, additional and facet features, one code at a time with app.hawksearch.features; any feature, including paid add-ons, so an app with it is never auto-approved), hawksearch:index (rebuild the user's engine's index from its feed and follow the rebuild with app.hawksearch.index, only where the engine's EnableRebuildIndex system parameter is on, one rebuild at a time per engine, and read the engine's indexes, current index, hierarchy and mappings through HawkSearch's Indexing API; never auto-approved), hawksearch:index:items (add, update and delete items and their attributes in the engine's indexes through the Indexing API, live at once on the current index; at most 125 items a call; never auto-approved), hawksearch:index:manage (create indexes, set the current one, delete others, rebuild autocomplete and the rest, hierarchy and facet mappings through the Indexing API; never auto-approved), hawksearch:recommendations (HawkSearch's recommendation widgets for the user's current engine with app.hawksearch.recommendations, never tracked in the client's recommendation reports; limited per app and client per minute), storage (the app's own key-value data and files kept by the platform with app.storage and app.storage.files, per app and client, within quotas; shared by everyone at the client and the app's functions and schedules) and assistant (ask the host's assistant, Hawk Assistant in HawkSearch, as the user with app.assistant.ask, from a click; the user sees the question and the answer in the assistant's chat; show the buttons only while context.features.assistant is true) and hawkai:read (list the client's HawkAI agents and smart responses for the user's current engine with app.hawkai: names and settings, never prompts).",
103
103
  "type": "array",
104
104
  "items": {
105
105
  "enum": [
@@ -119,7 +119,8 @@
119
119
  "hawksearch:index:manage",
120
120
  "hawksearch:recommendations",
121
121
  "storage",
122
- "assistant"
122
+ "assistant",
123
+ "hawkai:read"
123
124
  ]
124
125
  },
125
126
  "uniqueItems": true
package/src/agents.mjs CHANGED
@@ -1,10 +1,12 @@
1
1
  // Instructions for AI coding assistants, written into each app: AGENTS.md (read by Copilot, Cursor, Codex and others),
2
- // a CLAUDE.md that imports it (Claude Code), and the CLI's MCP server (`hawk-app mcp`) in .mcp.json (Claude Code) and
3
- // .vscode/mcp.json (VS Code / Copilot). The CLI owns the part of AGENTS.md between its markers and refreshes
4
- // it with `hawk-app upgrade`; anything outside the markers is the developer's. Sources: ../agents/AGENTS.md plus one
5
- // project section per template (../agents/project/), or custom.md for an app set up with `hawk-app init`.
2
+ // a CLAUDE.md that imports it (Claude Code), and the CLI's MCP server (`hawk-app mcp`) in .vscode/mcp.json (VS Code /
3
+ // Copilot). Claude Code gets the server from the Hawk Apps plugin (plugins/hawk-apps), installed once per machine, so
4
+ // the CLI no longer writes .mcp.json and takes its old `hawk-apps` entry out (the plugin's would show every tool twice).
5
+ // The CLI owns the part of AGENTS.md between its markers and refreshes it with `hawk-app upgrade`; anything outside
6
+ // the markers is the developer's. Sources: ../agents/AGENTS.md plus one project section per template
7
+ // (../agents/project/), or custom.md for an app set up with `hawk-app init`.
6
8
  import { existsSync, readFileSync } from 'node:fs';
7
- import { mkdir, writeFile } from 'node:fs/promises';
9
+ import { mkdir, rm, writeFile } from 'node:fs/promises';
8
10
  import { dirname, join } from 'node:path';
9
11
  import { fileURLToPath } from 'node:url';
10
12
 
@@ -20,7 +22,6 @@ const CLAUDE_IMPORT = '@AGENTS.md';
20
22
  */
21
23
  const CLI_BIN = 'node_modules/@bridgeline-digital/hawk-app-cli/bin/hawk-app.mjs';
22
24
  const MCP_CONFIGS = [
23
- { file: '.mcp.json', key: 'mcpServers', entry: { command: 'node', args: [CLI_BIN, 'mcp'] } },
24
25
  {
25
26
  file: '.vscode/mcp.json',
26
27
  key: 'servers',
@@ -28,6 +29,27 @@ const MCP_CONFIGS = [
28
29
  },
29
30
  ];
30
31
  const MCP_SERVER = 'hawk-apps';
32
+ /** Claude Code's project MCP settings: the plugin starts the server now, so the CLI's old entry comes out. */
33
+ const CLAUDE_MCP = '.mcp.json';
34
+
35
+ /** .mcp.json without the CLI's old `hawk-apps` server: 'remove' (other settings kept), 'delete' (nothing else), or 'current'. */
36
+ function planClaudeMcpCleanup(dir) {
37
+ const full = join(dir, CLAUDE_MCP);
38
+ if (!existsSync(full)) return null;
39
+ const text = readFileSync(full, 'utf8');
40
+ let config;
41
+ try {
42
+ config = JSON.parse(text);
43
+ } catch {
44
+ return { file: CLAUDE_MCP, state: 'current', content: text };
45
+ }
46
+ if (!config?.mcpServers?.[MCP_SERVER]) return { file: CLAUDE_MCP, state: 'current', content: text };
47
+ const { [MCP_SERVER]: _ours, ...servers } = config.mcpServers;
48
+ const updated = { ...config, mcpServers: servers };
49
+ if (Object.keys(servers).length === 0) delete updated.mcpServers;
50
+ if (Object.keys(updated).length === 0) return { file: CLAUDE_MCP, state: 'delete', content: '' };
51
+ return { file: CLAUDE_MCP, state: 'remove', content: JSON.stringify(updated, null, 2) + (text.includes('\r\n') ? '\r\n' : '\n') };
52
+ }
31
53
 
32
54
  /** An assistant's MCP settings with the hawk-apps server: created, added to the developer's own, or left as is. */
33
55
  function planMcpConfig(dir, { file, key, entry }) {
@@ -85,6 +107,8 @@ export function planAgentFiles(dir, app) {
85
107
  }
86
108
 
87
109
  for (const config of MCP_CONFIGS) files.push(planMcpConfig(dir, config));
110
+ const cleanup = planClaudeMcpCleanup(dir);
111
+ if (cleanup) files.push(cleanup);
88
112
 
89
113
  const claudeFile = join(dir, 'CLAUDE.md');
90
114
  if (!existsSync(claudeFile)) {
@@ -108,6 +132,10 @@ export const isAgentChange = (f) => f.state !== 'current' && f.state !== 'skippe
108
132
  export async function writeAgentFiles(dir, plan) {
109
133
  const changed = plan.filter(isAgentChange);
110
134
  for (const f of changed) {
135
+ if (f.state === 'delete') {
136
+ await rm(join(dir, f.file), { force: true });
137
+ continue;
138
+ }
111
139
  await mkdir(dirname(join(dir, f.file)), { recursive: true });
112
140
  await writeFile(join(dir, f.file), f.content);
113
141
  }
@@ -118,8 +146,11 @@ export async function writeAgentFiles(dir, plan) {
118
146
  export function describeAgentFile({ file, state }) {
119
147
  const mcp = file.endsWith('mcp.json');
120
148
  switch (state) {
149
+ case 'remove':
150
+ case 'delete':
151
+ return `${file}: the hawk-apps MCP server is ${state === 'delete' ? 'removed (the file had nothing else)' : 'taken out (your other servers are kept)'}: Claude Code gets it from the Hawk Apps plugin now (see the CLI README).`;
121
152
  case 'create':
122
- if (mcp) return `${file}: added (the hawk-app MCP server for ${file === '.mcp.json' ? 'Claude Code' : 'VS Code'}).`;
153
+ if (mcp) return `${file}: added (the hawk-app MCP server for VS Code).`;
123
154
  return file === 'AGENTS.md' ? 'AGENTS.md: added (instructions for AI coding assistants).' : `${file}: added (points Claude Code at AGENTS.md).`;
124
155
  case 'update':
125
156
  return `${file}: the CLI's part is refreshed (your notes outside its markers are kept).`;
@@ -0,0 +1,67 @@
1
+ // `hawk-app claude`: installs the Hawk Apps plugin for Claude Code (the hawk-apps MCP server and skills), once per
2
+ // machine. The plugin ships in this package (../claude-plugin: a marketplace with one plugin) so nobody needs access to
3
+ // the repository: it is copied next to the CLI's credentials, stamped with this CLI's version (Claude Code updates a
4
+ // plugin when its version changes), and added to Claude Code as a local marketplace. With the `claude` command on the
5
+ // PATH that happens here; otherwise the two /plugin commands are printed for the Claude Code panel. Run it again to
6
+ // update.
7
+ import { spawnSync } from 'node:child_process';
8
+ import { readFileSync } from 'node:fs';
9
+ import { cp, mkdir, rm, writeFile } from 'node:fs/promises';
10
+ import { dirname, join } from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+ import { cliVersion } from './channel.mjs';
13
+ import { credentialsPath } from './credentials.mjs';
14
+
15
+ export const MARKETPLACE = 'hawk-apps';
16
+ export const PLUGIN = 'hawk-apps';
17
+ export const PLUGIN_ID = `${PLUGIN}@${MARKETPLACE}`;
18
+ const SOURCE = fileURLToPath(new URL('../claude-plugin', import.meta.url));
19
+
20
+ /** Where the plugin is kept on this machine: next to the CLI's credentials (%APPDATA%\hawk-app\claude-plugin on Windows). */
21
+ export const pluginHome = (env = process.env) => join(dirname(credentialsPath(env)), 'claude-plugin');
22
+
23
+ /** Copies the packaged marketplace to `target` (replacing an older copy), with the plugin's version set to `version`. */
24
+ export async function writePlugin(target, version = cliVersion()) {
25
+ await rm(target, { recursive: true, force: true });
26
+ await mkdir(target, { recursive: true });
27
+ await cp(SOURCE, target, { recursive: true });
28
+ const manifest = join(target, PLUGIN, '.claude-plugin', 'plugin.json');
29
+ const plugin = JSON.parse(readFileSync(manifest, 'utf8'));
30
+ await writeFile(manifest, JSON.stringify({ ...plugin, version }, null, 2) + '\n');
31
+ }
32
+
33
+ /** Runs `claude plugin …`; null when there's no `claude` command. */
34
+ function defaultRunClaude(args) {
35
+ const result = spawnSync('claude', ['plugin', ...args], { encoding: 'utf8', shell: process.platform === 'win32' });
36
+ if (result.error || (result.status !== 0 && /not recognized|not found|ENOENT/i.test(`${result.stderr}${result.error?.message ?? ''}`))) return null;
37
+ return { ok: result.status === 0, output: `${result.stdout ?? ''}${result.stderr ?? ''}`.trim() };
38
+ }
39
+
40
+ /**
41
+ * Installs (or updates) the plugin. The marketplace is added, or refreshed when it already is; the plugin installed, or
42
+ * updated when it already is. Returns 0; the steps Claude Code still needs are printed when `claude` isn't available.
43
+ */
44
+ export async function claudeCommand({ log = console.log, env = process.env, runClaude = defaultRunClaude, version = cliVersion() } = {}) {
45
+ const target = pluginHome(env);
46
+ await writePlugin(target, version);
47
+ log(`✔ Hawk Apps plugin ${version} saved to ${target}`);
48
+
49
+ const added = runClaude(['marketplace', 'add', target]);
50
+ if (added === null) {
51
+ log('');
52
+ log('Now, in the Claude Code panel (or a `claude` session), type these two commands:');
53
+ log(` /plugin marketplace add ${target}`);
54
+ log(` /plugin install ${PLUGIN_ID}`);
55
+ log('Already installed? Update it instead: /plugin marketplace update hawk-apps, then /reload-plugins.');
56
+ return 0;
57
+ }
58
+ const marketplace = added.ok ? added : runClaude(['marketplace', 'update', MARKETPLACE]);
59
+ if (!marketplace?.ok) throw new Error(`Claude Code didn't add the plugin's marketplace:\n${added.output}`);
60
+ const installed = runClaude(['install', PLUGIN_ID, '--scope', 'user']);
61
+ const plugin = installed?.ok ? installed : runClaude(['update', PLUGIN_ID]);
62
+ if (!plugin?.ok) throw new Error(`Claude Code didn't install the plugin:\n${installed?.output ?? ''}`);
63
+ log(`✔ Claude Code has the ${PLUGIN_ID} plugin. In a session that's already open, run /reload-plugins.`);
64
+ log(' Its skills: /hawk-apps:new-screen, /hawk-apps:add-query, /hawk-apps:add-function, /hawk-apps:prepare-review;');
65
+ log(' its MCP server starts in the open folder (/mcp lists it). Run `npx @bridgeline-digital/hawk-app-cli@latest claude` again to update.');
66
+ return 0;
67
+ }
package/src/cli.mjs CHANGED
@@ -37,6 +37,7 @@ Start
37
37
  create <folder> Start a new app from a template (react, preact, next, next-mui, vanilla)
38
38
  init Make an existing project a Hawk App
39
39
  sso Sign in with your Bridgeline account (once per machine; CI: login with a publisher key)
40
+ claude Install the Hawk Apps plugin for Claude Code: MCP server and skills (once per machine)
40
41
  doctor Check everything: Node, login, hawk-app.json, project, scopes, template, MCP
41
42
  docs [topic] The developer docs: guides, the bridge API, every command (Bridgeline sign-in)
42
43
 
@@ -146,7 +147,12 @@ Usage:
146
147
  deletes one of yours (no clients on it)
147
148
  hawk-app why <client id> What a client's users see of the app, and why
148
149
  hawk-app mcp Run the MCP server for AI coding assistants (stdio; dev platform
149
- only). create and init add it to .mcp.json and .vscode/mcp.json
150
+ only). create and init add it to .vscode/mcp.json; Claude Code
151
+ gets it from the Hawk Apps plugin (hawk-app claude)
152
+ hawk-app claude Install or update the Hawk Apps plugin for Claude Code (the MCP
153
+ server and skills), once per machine: copied next to your
154
+ login, then added with \`claude plugin\` when it's on the PATH,
155
+ else the two /plugin commands to type in Claude Code
150
156
  hawk-app dev [--client <id>] [--engine <id>] [--url <app dev URL>] [--port 3200] [--client-user] [--no-start]
151
157
  Run the app locally in a stand-in for HawkSearch, with real
152
158
  tokens and data for any client (runs \`npm run dev\` unless
@@ -213,6 +219,8 @@ HawkSearch index (scope hawksearch:index, no commands): app.hawksearch.index.reb
213
219
  Creating, switching and deleting indexes, hierarchy and mappings: scope hawksearch:index:manage.
214
220
  HawkSearch recommendations (scope hawksearch:recommendations, no commands): app.hawksearch.recommendations from the
215
221
  bridge, the engine's recommendation widgets, never tracked in the client's recommendation reports.
222
+ HawkAI (scope hawkai:read, no commands): app.hawkai.agents/agent/smartResponses/smartResponse from the bridge, the
223
+ client's HawkAI agents and smart responses for the user's engine (names and settings, never prompts).
216
224
  Storage (scope storage, no commands): app.storage / ctx.platform.storage get, put, delete, list, batch and usage from
217
225
  the bridge, the app's own key-value data per client (JSON values up to 350 KB, optional expiry, version checks), and
218
226
  storage.files upload, url, download, list and delete for its files (up to 100 MB each).
@@ -353,6 +361,11 @@ export async function main(argv, { fetchImpl = fetch, log = console.log, runBuil
353
361
  case 'logout':
354
362
  return logout({ platform: flags.platform, env: flags.env, log });
355
363
 
364
+ case 'claude': {
365
+ const { claudeCommand } = await import('./claude-plugin.mjs');
366
+ return claudeCommand({ log });
367
+ }
368
+
356
369
  case 'mcp': {
357
370
  // Loaded only here: the MCP SDK isn't needed by any other command.
358
371
  const { serveMcp } = await import('./mcp.mjs');
package/src/doctor.mjs CHANGED
@@ -26,6 +26,9 @@ export function appUrl(platformUrl, slug, alias) {
26
26
  /** The developer docs (built from the hawk-apps repo's markdown; sign in with a Bridgeline account). */
27
27
  export const DOCS_URL = 'https://developers.hawksearchapps.com';
28
28
 
29
+ /** How to install (or update) the Hawk Apps plugin for Claude Code: see claude-plugin.mjs. */
30
+ export const PLUGIN_INSTALL = 'npx @bridgeline-digital/hawk-app-cli@latest claude';
31
+
29
32
  /**
30
33
  * `hawk-app docs <topic>`: the page for each topic. Developers' topics open the developer docs (a feature's section of
31
34
  * the bridge reference); `platform` opens the engineering docs, which the site keeps out of its navigation and search.
@@ -166,9 +169,14 @@ export async function doctorCommand({ manifestFile, flags = {}, fetchImpl = fetc
166
169
  }
167
170
 
168
171
  // AI assistants.
169
- const mcp = join(projectDir, '.mcp.json');
170
- if (existsSync(mcp) && readFileSync(mcp, 'utf8').includes('hawk-apps')) ok('MCP server set up for AI assistants (.mcp.json)');
171
- else warn('No MCP config for AI assistants: `npx hawk-app upgrade --yes` adds .mcp.json and .vscode/mcp.json.');
172
+ const vscodeMcp = join(projectDir, '.vscode', 'mcp.json');
173
+ if (existsSync(vscodeMcp) && readFileSync(vscodeMcp, 'utf8').includes('hawk-apps')) ok('MCP server set up for VS Code (.vscode/mcp.json)');
174
+ else warn('No MCP config for VS Code: `npx hawk-app upgrade --yes` adds .vscode/mcp.json.');
175
+ const claudeMcp = join(projectDir, '.mcp.json');
176
+ if (existsSync(claudeMcp) && readFileSync(claudeMcp, 'utf8').includes('"hawk-apps"')) {
177
+ warn('.mcp.json still starts the hawk-apps MCP server: `npx hawk-app upgrade --yes` takes it out (the Hawk Apps plugin starts it for Claude Code).');
178
+ }
179
+ log(` Claude Code: the Hawk Apps plugin gives it the MCP server and skills (${PLUGIN_INSTALL}).`);
172
180
 
173
181
  log(failed ? `\n${failed} thing${failed === 1 ? '' : 's'} to fix above.` : '\nAll set.');
174
182
  log(`Docs: ${DOCS_URL} (npx hawk-app docs)`);
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'];
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'];
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'];
@@ -156,9 +156,18 @@ export function bridgeCall(api, method, path, rules = FALLBACK) {
156
156
  }
157
157
  if (api === 'search') {
158
158
  const limit = `${rules.search.perMinute} calls a minute.`;
159
- if (rel === 'api/v2/search') return { call: "await app.hawksearch.search({ Keyword: 'boots' })", scope: 'hawksearch:search', note: `The platform sets ClientGuid to the engine's; IsInPreview: true runs on the preview search API. ${limit}` };
159
+ if (rel === 'api/v2/search') {
160
+ return {
161
+ call: "await app.hawksearch.search({ Keyword: 'boots' })",
162
+ scope: 'hawksearch:search',
163
+ note: `The platform sets ClientGuid to the engine's; IsInPreview: true runs on the preview search API (with SmartBar switches and PreviewDate, ClientData.PreviewBuckets for visitor targets). ${limit}`,
164
+ };
165
+ }
160
166
  if (rel === 'api/v2/autocomplete') return { call: "await app.hawksearch.autocomplete({ Keyword: 'boo' })", scope: 'hawksearch:search', note: `The platform sets ClientGuid to the engine's. ${limit}` };
161
- return { unavailable: 'Apps can call only search and autocomplete of the Search API.' };
167
+ return {
168
+ unavailable:
169
+ "Apps can call only search and autocomplete of the Search API, plus the preview page's tools: app.hawksearch.preview.settings(), indexes(), item(id) and explain(request) (hawksearch:read).",
170
+ };
162
171
  }
163
172
  if (api === 'recommendations') {
164
173
  const { available, perMinute, maxBulkWidgets } = rules.recommendations;
@@ -73,8 +73,8 @@ export async function checkProject(dir) {
73
73
  }
74
74
  }
75
75
 
76
- if (pkg && !pkg.devDependencies?.[CLI] && !pkg.dependencies?.[CLI] && (existsSync(join(dir, '.mcp.json')) || existsSync(join(dir, '.vscode', 'mcp.json')))) {
77
- warnings.push(`${CLI} isn't a dev dependency, but the MCP settings start it from node_modules: npm install -D ${CLI}.`);
76
+ if (pkg && !pkg.devDependencies?.[CLI] && !pkg.dependencies?.[CLI] && existsSync(join(dir, '.vscode', 'mcp.json'))) {
77
+ warnings.push(`${CLI} isn't a dev dependency, but VS Code's MCP settings start it from node_modules: npm install -D ${CLI}.`);
78
78
  }
79
79
  if (!pkg) {
80
80
  warnings.push('No package.json: deploy uploads the folder as it is, and npm audit cannot run, so the review\'s dependencies check fails.');
@@ -9,7 +9,7 @@ import { extname, join, relative } from 'node:path';
9
9
  */
10
10
  const RULES = [
11
11
  { scope: 'reports:read', pattern: /\.analytics\.report\s*\(/ },
12
- { scope: 'hawksearch:read', pattern: /\.dashboard\.get\s*\(/ },
12
+ { scope: 'hawksearch:read', pattern: /\.dashboard\.get\s*\(|\.hawksearch\.preview\.(settings|indexes|item|explain)\s*\(/ },
13
13
  { scope: 'hawksearch:write', pattern: /\.dashboard\.(post|put|delete)\s*\(/ },
14
14
  { scope: 'data:read', pattern: /\.data\.query\s*\(/ },
15
15
  { scope: 'functions:invoke', pattern: /\.functions\.(call|start|job|wait)\s*\(/ },
@@ -25,6 +25,7 @@ const RULES = [
25
25
  { scope: 'lambdas:invoke', pattern: /\.lambdas\.invoke\s*\(/ },
26
26
  { scope: 'ai:invoke', pattern: /\.ai\.(generate|stream|info)\s*\(/ },
27
27
  { scope: 'assistant', pattern: /\.assistant\.(ask|setContext)\s*\(/ },
28
+ { scope: 'hawkai:read', pattern: /\.hawkai\.(agents|agent|smartResponses|smartResponse|runSmartResponse|assistant)\s*\(/ },
28
29
  ];
29
30
 
30
31
  const SOURCE = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.vue', '.svelte', '.html']);