@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 +46 -17
- package/agents/AGENTS.md +8 -4
- package/claude-plugin/.claude-plugin/marketplace.json +14 -0
- package/claude-plugin/hawk-apps/.claude-plugin/plugin.json +8 -0
- package/claude-plugin/hawk-apps/.mcp.json +8 -0
- package/claude-plugin/hawk-apps/scripts/mcp.mjs +35 -0
- package/claude-plugin/hawk-apps/skills/add-function/SKILL.md +34 -0
- package/claude-plugin/hawk-apps/skills/add-query/SKILL.md +31 -0
- package/claude-plugin/hawk-apps/skills/new-screen/SKILL.md +32 -0
- package/claude-plugin/hawk-apps/skills/prepare-review/SKILL.md +32 -0
- package/docs/bridge/README.md +69 -3
- package/docs/showcase/hawk-app.json +8 -3
- package/docs/showcase/src/App.tsx +235 -1
- package/docs/ui/README.md +2 -2
- package/package.json +2 -1
- package/schema/hawk-app.schema.json +3 -2
- package/src/agents.mjs +38 -7
- package/src/claude-plugin.mjs +67 -0
- package/src/cli.mjs +14 -1
- package/src/doctor.mjs +11 -3
- package/src/manifest.mjs +1 -1
- package/src/mcp-hawksearch.mjs +11 -2
- package/src/project-checks.mjs +2 -2
- package/src/scope-check.mjs +2 -1
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
|
|
348
|
-
|
|
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
|
|
373
|
-
|
|
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 |
|
|
376
|
-
| --- | --- |
|
|
377
|
-
| `check-before-submit` |
|
|
378
|
-
| `add-feature` |
|
|
379
|
-
| `explain-app` |
|
|
380
|
-
| `deploy-to-client` |
|
|
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
|
|
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 `.
|
|
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,
|
|
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
|
-
|
|
96
|
-
|
|
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
|
|
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,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.
|
package/docs/bridge/README.md
CHANGED
|
@@ -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.
|
|
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 (
|
|
3
|
-
// .
|
|
4
|
-
//
|
|
5
|
-
//
|
|
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
|
|
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
|
|
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
|
|
170
|
-
if (existsSync(
|
|
171
|
-
else warn('No MCP config for
|
|
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'];
|
package/src/mcp-hawksearch.mjs
CHANGED
|
@@ -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')
|
|
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 {
|
|
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;
|
package/src/project-checks.mjs
CHANGED
|
@@ -73,8 +73,8 @@ export async function checkProject(dir) {
|
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
if (pkg && !pkg.devDependencies?.[CLI] && !pkg.dependencies?.[CLI] &&
|
|
77
|
-
warnings.push(`${CLI} isn't a dev dependency, but
|
|
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.');
|
package/src/scope-check.mjs
CHANGED
|
@@ -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']);
|