@nullplatform/mcp 0.1.13 → 0.1.14
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/dist/np/context.js +6 -2
- package/dist/surfaces/developer.js +9 -2
- package/dist/tools/approvals.js +1 -1
- package/dist/tools/builds.js +1 -1
- package/dist/tools/create-app.js +40 -28
- package/dist/tools/create-link.js +1 -1
- package/dist/tools/create-release.js +1 -1
- package/dist/tools/create-scope.js +1 -1
- package/dist/tools/create-service.js +12 -12
- package/dist/tools/delete-link.js +1 -1
- package/dist/tools/delete-param.js +1 -1
- package/dist/tools/delete-service.js +1 -1
- package/dist/tools/deploy.js +1 -1
- package/dist/tools/deployments.js +1 -1
- package/dist/tools/entity-get.js +1 -1
- package/dist/tools/entity-list.js +1 -1
- package/dist/tools/find-apps.js +5 -2
- package/dist/tools/logs.js +1 -1
- package/dist/tools/metrics.js +1 -1
- package/dist/tools/overview.js +1 -1
- package/dist/tools/params.js +1 -1
- package/dist/tools/playbook.js +1 -1
- package/dist/tools/releases.js +1 -1
- package/dist/tools/services.js +1 -1
- package/dist/tools/set-params.js +1 -1
- package/dist/tools/shared.js +43 -0
- package/dist/tools/status.js +1 -1
- package/dist/tools/traffic.js +1 -1
- package/dist/tools/update-link.js +1 -1
- package/dist/tools/update-service.js +1 -1
- package/package.json +4 -1
- package/widgets-dist/approvals.html +9 -1
- package/widgets-dist/builds.html +9 -1
- package/widgets-dist/create-app.html +18 -10
- package/widgets-dist/deployments.html +9 -1
- package/widgets-dist/find-apps.html +9 -1
- package/widgets-dist/logs.html +9 -1
- package/widgets-dist/manifest.json +16 -16
- package/widgets-dist/metrics.html +9 -1
- package/widgets-dist/np-panel.html +9 -1
- package/widgets-dist/overview.html +9 -1
- package/widgets-dist/params.html +22 -14
- package/widgets-dist/releases.html +9 -1
- package/widgets-dist/service-action.html +9 -1
- package/widgets-dist/service-create.html +9 -1
- package/widgets-dist/service-delete.html +9 -1
- package/widgets-dist/service-link.html +9 -1
- package/widgets-dist/services.html +15 -7
package/dist/np/context.js
CHANGED
|
@@ -124,9 +124,13 @@ export class NpContext {
|
|
|
124
124
|
/** Search apps by partial name (and/or namespace name) across the whole org — parallel + cached. */
|
|
125
125
|
async findApps(args = {}) {
|
|
126
126
|
const skeleton = await this.getSkeleton();
|
|
127
|
-
|
|
127
|
+
// Match namespace names leniently: strip case AND non-alphanumerics on both sides so "pablov",
|
|
128
|
+
// "pablo v" and "Pablo-V" all resolve the namespace displayed as "Pablo V" (a strict substring
|
|
129
|
+
// missed it on the space, which is why a namespace query fell back to dumping the whole org).
|
|
130
|
+
const normalize = (text) => text.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
131
|
+
const namespaceFilter = args.namespace ? normalize(args.namespace) : undefined;
|
|
128
132
|
const namespaces = namespaceFilter
|
|
129
|
-
? skeleton.namespaces.filter((namespace) => namespace.name
|
|
133
|
+
? skeleton.namespaces.filter((namespace) => normalize(namespace.name).includes(namespaceFilter))
|
|
130
134
|
: skeleton.namespaces;
|
|
131
135
|
const nameFilter = args.query?.toLowerCase();
|
|
132
136
|
// `/application` is namespace-scoped with no name filter, so page each namespace to completion
|
|
@@ -14,9 +14,16 @@ Every tool accepts \`language\`: ALWAYS set it to the language the user is conve
|
|
|
14
14
|
|
|
15
15
|
Most tools render an interactive panel in clients that support it — apps, status, builds, releases, deployments, logs, metrics, parameters and approvals all appear as live UI. **When a tool's panel renders, that panel IS the answer: do not reproduce its data in your text reply.** The user already sees every row, status and value. NEVER print a markdown table of the same rows, re-list the items, or restate per-row status — duplicating the panel in text is the single most common mistake. Reply with AT MOST one short sentence — the one key takeaway or the next step — or nothing at all. A one-line insight the panel doesn't itself show is fine ("builds 3 and 5 were never released"); re-rendering the list as a table is not.
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
NEVER render a panel-rendering read MORE THAN ONCE to answer one question — not in a wait loop, and not fanned out across several apps/scopes. Two shapes of this mistake:
|
|
18
|
+
- POLLING a changing status (a build finishing, a deployment rolling out, an app going active): do the waiting with the DATA-ONLY reads (\`entity_get\`/\`entity_list\`, which render NO panel) — e.g. loop \`entity_get entity:"build" id:<id>\` until its status is final. Re-calling \`application_get\` once per poll repaints the whole panel every iteration and stacks identical panels down the chat (the "looping on app_get" mistake).
|
|
19
|
+
- AGGREGATING or COMPARING across many entities ("the latest release across all my apps", "which scope is on the newest build", "do any apps have failing builds"): do NOT call \`application_release_list\`/\`application_build_list\`/… once per app and dump a panel for each — that floods the chat and still doesn't answer the question. Gather headlessly with \`entity_list\` per app (\`entity_list entity:"release" parent_type:"application" parent_id:<id>\`), compute the answer yourself (sort by created_at, pick the max), and ANSWER IT — in one sentence ("the newest is auth-api 0.0.2, cut today"), or by rendering a SINGLE panel for just the one app/entity worth showing. The user asked for an answer, not N lists to eyeball.
|
|
20
|
+
Render headlessly while you gather; show a panel at most once, for the one state worth seeing.
|
|
18
21
|
|
|
19
|
-
|
|
22
|
+
EVERY form this server opens follows one contract — app creation, service provisioning, scope creation, parameter setting, service linking, release creation, all of them. (1) GATHER what you need to pre-fill BEFORE opening the form: settle structural choices and ask for the missing context first, and ask any enumerable choice INTERACTIVELY (the client's question/choice tool with concrete options) so the user clicks rather than reads a typed list — free-text fields the form already has (a name, a URL) you let the form collect. (2) Open the form ONCE, pre-filled from your inference: carry each best-guess in as the field's hint (a namespace, a stack/template, a service \`type\`…) so the tool pre-selects it — never describe the choice in prose and leave the picker blank, and never open a blank or half-filled form and keep asking. (3) Once the form is on screen, NEVER ask a clarifying question or fire a question tool about a field the form already covers — those controls ARE the form; you only react to what it reports back. The rest of this section is that contract applied to app creation; the same three rules hold for every other form.
|
|
23
|
+
|
|
24
|
+
When the user wants to create, scaffold, set up or import an application, drive it through the \`application_create\` FORM — its panel collects the namespace, template and repository. Two rules keep this clean. (1) Settle any genuinely STRUCTURAL question about WHAT to build — e.g. "is this one app or two, a frontend and an API?" — in one short turn BEFORE opening any form, since each app gets its own form. (2) Never gather FORM-FIELD details (namespace, template, new-vs-import repository, monorepo path) in conversation: INFER the best-fitting namespace and account from what the app is FOR — a demo → a demos/examples namespace, a backend service → a services namespace — and pass them as \`namespace\`/\`account\` (plus \`name\` if given) so the form opens pre-selected on your inference; the user changes it in the form if they want. When the request gives you NOTHING to infer from — a bare "create an app" with no hint of what it's for or what to call it — FINISH gathering context BEFORE opening any form: ask what the app is FOR (this fixes the namespace) and what to NAME it, and WAIT for both answers. Ask the "what is it for" part as an INTERACTIVE question — use the client's question/choice tool (e.g. AskUserQuestion) with concrete options (a backend API, a frontend, a demo, a worker/queue consumer, a CLI…) so the user picks in one click instead of reading a typed list; the NAME is free text, so ask it alongside or let the form's NAME field collect it, but never force a name into a multiple-choice question. Only THEN open the form, once, fully pre-filled — namespace inferred from the purpose, name carried in via \`name\`. Do NOT open the form "in the meantime", "while you're at it", or "to save time" with a question still pending: the form comes AFTER the context is in hand, never alongside an open question, and never half-filled while you keep asking. Opening a blank or partial form and continuing to ask is the form → question anti-pattern and the single worst thing you can do here. So: enough context already in the request → open the form pre-filled, no questions; missing context → gather ALL of it first (purpose AND name), THEN the one fully pre-filled form. Once a form is on screen, NEVER ask a clarifying question or use any question-asking tool about a field it covers — above all NEVER ask "which namespace?". The form ALREADY contains a "New repository | Import existing" toggle, a namespace picker, a template picker and a monorepo path field — so NEVER, after opening it, present a question like "How should the repository be set up?" with "New from template / Import existing" options, and never re-ask namespace or template: those controls ARE the form, and duplicating them as a follow-up question is the exact mistake to avoid (it is what produces the form-then-question screen). Any question you genuinely need comes BEFORE the form and the form opens AFTER it, pre-filled; once the form is up, the only thing you do is react to what it reports back.
|
|
25
|
+
|
|
26
|
+
Creating a NEW app is the PLATFORM's job, never a local scaffold. nullplatform creates the GitHub repository from a template and provisions it — so for a new app you must NEVER run a generic brainstorm/design/implementation process, never \`git init\`, never create an empty repo, and never write code locally first. The flow is: settle what to build → \`application_create\` with a new repo and ONE OF THE PLATFORM'S TEMPLATES → the platform creates the repo on GitHub → THEN clone it, add code, push so CI builds. The available STACKS are exactly those templates for the chosen namespace — never invent or offer a stack of your own ("Next.js", "Express", …); the form lists the real templates, choose from those. Carry your stack inference into the form the SAME way you carry the namespace: pass \`template\` with the stack you'd pick from what the app is FOR (a Go API → "go", a React frontend → "react"/"node", a Python service → "python") and the form opens with that template pre-selected — don't just name the best fit in prose and leave the field blank. This pre-fill-from-inference rule is general: every create/fill form (template, service \`type\`, scope, …) pre-selects from the hint you pass, so always pass the hint rather than describing the choice and leaving the picker empty. And do NOT fire the rendering reads (\`application_list\`/\`application_get\`/\`application_build_list\`…) just to orient yourself before any of this — each renders a panel the user didn't ask for. To gather data for your OWN reasoning, navigate the entity tree headlessly with \`entity_list\` and \`entity_get\` — read-only, DATA-only, NO panel. \`entity_list\` lists a collection scoped to its parent (the tree is organization → account → namespace → application → {scope, build, release}; scope → deployment): e.g. \`entity_list entity:"application" parent_type:"namespace" parent_id:11\`, then \`entity_list entity:"build" parent_type:"application" parent_id:123\`. \`entity_get entity:"application" id:123\` reads one. Parameters, services, links and approvals list the same way under an application or scope (\`entity_list entity:"parameter" parent_type:"application" parent_id:123\`). That is how you learn where apps live, how they're named, and what's deployable without putting a list on the user's screen. Reserve the rendered reads (\`application_list\`/\`application_get\`/\`application_build_list\`/…) for when the user actually wants to SEE that data.
|
|
20
27
|
|
|
21
28
|
Typical flows:
|
|
22
29
|
- Ship: \`application_deployment_create\` (picks the latest build, cuts the release for you) → \`application_deployment_update percent:25/50/100\` → \`application_deployment_update action:"finalize"\`.
|
package/dist/tools/approvals.js
CHANGED
|
@@ -14,7 +14,7 @@ import { appArg, requireApp } from "./shared.js";
|
|
|
14
14
|
export const approvalsTool = defineTool({
|
|
15
15
|
name: TOOL.applicationApprovalList,
|
|
16
16
|
title: "Approvals",
|
|
17
|
-
description: 'List the approvals gating an application\'s actions (e.g. a deployment waiting on a policy), and act on one. action:"approve" lets the gated action proceed; action:"cancel" cancels the request
|
|
17
|
+
description: 'List the approvals gating an application\'s actions (e.g. a deployment waiting on a policy), rendered as a PANEL, and act on one. action:"approve" lets the gated action proceed; action:"cancel" cancels the request — both run with YOUR own permissions, so the platform denies what you\'re not allowed to do. Use it when "a deploy is stuck", "creating_approval", "approve the deployment", "what\'s waiting on approval". Does NOT deploy or change traffic — it only resolves the gate; the gated action proceeds on its own once approved.',
|
|
18
18
|
annotations: { destructiveHint: false, openWorldHint: true },
|
|
19
19
|
widget: "approvals",
|
|
20
20
|
errorKey: "approvals.errorLabel",
|
package/dist/tools/builds.js
CHANGED
|
@@ -13,7 +13,7 @@ import { appArg, offsetArg, pageOf, requireApp } from "./shared.js";
|
|
|
13
13
|
export const buildsTool = defineTool({
|
|
14
14
|
name: TOOL.applicationBuildList,
|
|
15
15
|
title: "Builds",
|
|
16
|
-
description: "List
|
|
16
|
+
description: "List ONE application's recent CI builds as a PANEL for the user — status, branch, commit, age, and whether each is already released. Use it to answer 'did my push build yet?' and to pick a build_id to deploy. Pass build:<id> to see that build's assets, or commit:<sha> to find the build for a specific commit. Do NOT call this once per app to compare across apps, and do NOT loop it to wait for a build to finish (it renders a panel each time): for an AGGREGATE across apps use entity_list (entity:\"build\"), and to POLL a build to completion use entity_get (entity:\"build\" id:<id>) — both headless.",
|
|
17
17
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
18
18
|
widget: "builds",
|
|
19
19
|
errorKey: "builds.errorLabel",
|
package/dist/tools/create-app.js
CHANGED
|
@@ -4,24 +4,24 @@ import { dashboardLink, glyph, linkLine, next } from "../md.js";
|
|
|
4
4
|
import { createApplication, getApplication, listTemplates } from "../np/journey.js";
|
|
5
5
|
import { defineTool, fail, reply } from "../tool.js";
|
|
6
6
|
import { TOOL } from "../tool-names.js";
|
|
7
|
-
import { delays, httpsRepoUrl, sleep } from "./shared.js";
|
|
8
|
-
function pickNamespace(namespaces, hints) {
|
|
7
|
+
import { delays, httpsRepoUrl, resolveChoice, sleep } from "./shared.js";
|
|
8
|
+
export function pickNamespace(namespaces, hints) {
|
|
9
|
+
// Namespace is the generic resolveChoice with one extra twist — account narrowing. An explicit id is
|
|
10
|
+
// unambiguous, so honour it against the FULL list FIRST (the account filter only disambiguates a
|
|
11
|
+
// repeated NAME; it must never veto a real id). Then resolve the name within the named account, and
|
|
12
|
+
// fall back to all namespaces so a correct name still pins even when the account label doesn't line up.
|
|
13
|
+
const byId = resolveChoice(namespaces, { id: hints.namespace_id });
|
|
14
|
+
if (byId)
|
|
15
|
+
return byId;
|
|
9
16
|
const accountQuery = hints.account?.toLowerCase();
|
|
10
|
-
const
|
|
17
|
+
const inAccount = accountQuery
|
|
11
18
|
? namespaces.filter((candidate) => (candidate.account_name ?? "").toLowerCase().includes(accountQuery))
|
|
12
19
|
: namespaces;
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
if (hints.namespace) {
|
|
20
|
-
const query = hints.namespace.toLowerCase();
|
|
21
|
-
const matches = candidates.filter((candidate) => candidate.name.toLowerCase().includes(query));
|
|
22
|
-
if (matches.length === 1)
|
|
23
|
-
return matches[0];
|
|
24
|
-
}
|
|
20
|
+
const named = resolveChoice(inAccount.length ? inAccount : namespaces, { text: hints.namespace }) ??
|
|
21
|
+
resolveChoice(namespaces, { text: hints.namespace });
|
|
22
|
+
if (named)
|
|
23
|
+
return named;
|
|
24
|
+
const candidates = inAccount.length ? inAccount : namespaces;
|
|
25
25
|
if (candidates.length === 1)
|
|
26
26
|
return candidates[0];
|
|
27
27
|
return undefined;
|
|
@@ -41,11 +41,11 @@ async function buildFormReply(context, prefill) {
|
|
|
41
41
|
// of the global set. Fetch only once a namespace is pinned (the widget refetches per namespace on
|
|
42
42
|
// change); the org-level NRN returns a misleading near-empty subset.
|
|
43
43
|
const templates = picked ? await listTemplates(context.np, picked.nrn).catch(() => []) : [];
|
|
44
|
-
// Same
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
// Same generic rule as the namespace: resolve the model's stack hint (`template`) — or an explicit
|
|
45
|
+
// template_id — against THIS namespace's templates, matching on name + tags, and pin the only one.
|
|
46
|
+
// Leave it null when it's a real unhinted choice; never silently scaffold the first stack in the list
|
|
47
|
+
// (a wrong stack is worse than a deliberate pick).
|
|
48
|
+
const templateId = resolveChoice(templates, { id: prefill.template_id, text: prefill.template }, { pinOnly: true, haystack: (template) => `${template.name} ${(template.tags ?? []).join(" ")}` })?.id ?? null;
|
|
49
49
|
const namespaces = skeleton.namespaces.map((namespace) => ({
|
|
50
50
|
id: namespace.id,
|
|
51
51
|
name: namespace.name,
|
|
@@ -75,7 +75,7 @@ async function buildFormReply(context, prefill) {
|
|
|
75
75
|
export const createAppTool = defineTool({
|
|
76
76
|
name: TOOL.applicationCreate,
|
|
77
77
|
title: "Create application",
|
|
78
|
-
description:
|
|
78
|
+
description: 'Create a nullplatform application. GATE FIRST: if the request is bare and gives you NOTHING to infer from (e.g. just "create an app" / "create a new application" with no hint of what it\'s for or what to name it), DO NOT call this tool yet — first ask the user, in chat, what the app is for AND what to name it, and WAIT for both; call this only once you have the purpose (to pre-fill the namespace) AND the name in hand. Do NOT call it in the meantime, while still waiting on the name — opening a blank or half-filled form and continuing to ask is a failure, not a fallback. When you DO have context, INFER the best-fitting namespace and account from what the app is FOR (a demo → a demos/examples namespace, a backend service → a services namespace) and pass them as `namespace`/`account` so the form opens pre-selected on your inference — the user can still change it. Calling without `provision:true` opens an interactive form and creates nothing — the user confirms the namespace, then either a NEW repository (scaffolded from a template, repo URL auto-generated) or IMPORTS an existing repository (optionally a monorepo folder), and the form itself submits with provision:true to actually create. Only the form\'s submit sets provision:true; you (the model) NEVER set it and NEVER pass a repository_url to "pre-fill" expecting a create — passing a repository_url without provision just opens the form, it does NOT write anything. Provisioning is async — the create waits briefly and reports progress.',
|
|
79
79
|
annotations: { destructiveHint: false, openWorldHint: true },
|
|
80
80
|
widget: "create-app",
|
|
81
81
|
errorKey: "createApp.errorLabel",
|
|
@@ -94,6 +94,10 @@ export const createAppTool = defineTool({
|
|
|
94
94
|
.string()
|
|
95
95
|
.optional()
|
|
96
96
|
.describe("Full git repo URL. New repos: auto-generated from the account + name. Import: the existing URL."),
|
|
97
|
+
template: z
|
|
98
|
+
.string()
|
|
99
|
+
.optional()
|
|
100
|
+
.describe('Template/stack to scaffold from — INFER it from what the app is FOR (a Go service → "go", a React frontend → "react"/"node", a Python API → "python") and pass the stack you would choose. The tool matches it against the chosen namespace\'s real templates and pre-selects it; prefer template_id only when you already know the exact id.'),
|
|
97
101
|
template_id: z
|
|
98
102
|
.number()
|
|
99
103
|
.optional()
|
|
@@ -102,22 +106,28 @@ export const createAppTool = defineTool({
|
|
|
102
106
|
.string()
|
|
103
107
|
.optional()
|
|
104
108
|
.describe("Import only: the folder inside a monorepo that holds this application"),
|
|
109
|
+
provision: z
|
|
110
|
+
.boolean()
|
|
111
|
+
.optional()
|
|
112
|
+
.describe("Set true ONLY to actually create the app + repository — this is the form's Create button. Omit it to open/pre-fill the form, even when you pass a repository_url: opening or pre-filling NEVER creates anything. You (the model) must NOT set this; the user confirms by submitting the form."),
|
|
105
113
|
},
|
|
106
114
|
async handler(args, context) {
|
|
107
|
-
// Create ONLY on an explicit
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
115
|
+
// Create ONLY on an explicit provision:true (the form's Create button). The presence of a
|
|
116
|
+
// repository_url is NOT the trigger: the model routinely passes one to PRE-FILL the form, and
|
|
117
|
+
// treating that as "create now" silently provisioned an app the user never confirmed (the bug
|
|
118
|
+
// where the panel jumped straight to "Creating…"). So opening or pre-filling the form — with or
|
|
119
|
+
// without a repository_url — creates nothing; the user submits to create. create_app also does
|
|
120
|
+
// NOT read the ambient git remote, so running the MCP inside some repo never auto-creates either.
|
|
112
121
|
const repoUrl = args.repository_url ? httpsRepoUrl(args.repository_url) : undefined;
|
|
113
|
-
if (!repoUrl) {
|
|
122
|
+
if (!args.provision || !repoUrl) {
|
|
114
123
|
return buildFormReply(context, {
|
|
115
124
|
name: args.name,
|
|
116
125
|
account: args.account,
|
|
117
126
|
namespace: args.namespace,
|
|
118
127
|
namespace_id: args.namespace_id,
|
|
128
|
+
template: args.template,
|
|
119
129
|
template_id: args.template_id,
|
|
120
|
-
repository_url: null,
|
|
130
|
+
repository_url: args.repository_url ?? null,
|
|
121
131
|
});
|
|
122
132
|
}
|
|
123
133
|
// Convergent under retries: a repo already linked to an app returns that app, never a duplicate.
|
|
@@ -187,6 +197,8 @@ export const createAppTool = defineTool({
|
|
|
187
197
|
name,
|
|
188
198
|
account: args.account,
|
|
189
199
|
namespace: args.namespace,
|
|
200
|
+
template: args.template,
|
|
201
|
+
template_id: args.template_id,
|
|
190
202
|
repository_url: repoUrl,
|
|
191
203
|
});
|
|
192
204
|
}
|
|
@@ -20,7 +20,7 @@ const slug = (text) => text
|
|
|
20
20
|
export const createLinkTool = defineTool({
|
|
21
21
|
name: TOOL.applicationLinkCreate,
|
|
22
22
|
title: "Link service",
|
|
23
|
-
description:
|
|
23
|
+
description: 'LINK a provisioned dependency service (a database, queue, cache, …) to an application or one of its scopes so its connection details flow in as read-only parameters. Use it for "connect/link the database to <app/scope>", "wire up the queue". Pass `service` (by name) and optionally a `scope` (default: the whole app). A credential link (e.g. a Postgres database-user) provisions a user with the permissions you pick, so call WITHOUT `create:true` first to open the form; the user confirms by submitting. New parameters reach the runtime on the NEXT deploy. Provision the service first with application_service_create.',
|
|
24
24
|
annotations: { destructiveHint: false, openWorldHint: true },
|
|
25
25
|
widget: "service-link",
|
|
26
26
|
errorKey: "createLink.errorLabel",
|
|
@@ -8,7 +8,7 @@ import { appArg, requireApp } from "./shared.js";
|
|
|
8
8
|
export const createReleaseTool = defineTool({
|
|
9
9
|
name: TOOL.applicationReleaseCreate,
|
|
10
10
|
title: "Create release",
|
|
11
|
-
description:
|
|
11
|
+
description: 'Cut an active RELEASE from a build (default: the latest successful build; semver auto-bumps the patch) — a release is a deployable, versioned pin of a build. Use it for "cut a release", "create a release from build X" WITHOUT deploying. Usually you do NOT need this directly: application_deployment_create cuts the release for you as part of shipping — reach for this only when the user explicitly wants a release but not a deploy. Convergent: reuses the existing active release for the same build rather than duplicating. Renders the releases panel; deploy a release with application_deployment_create.',
|
|
12
12
|
// Convergent under retries (reuses the active release-for-build), so idempotent.
|
|
13
13
|
annotations: { destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
14
14
|
widget: "releases",
|
|
@@ -9,7 +9,7 @@ import { buildAppStatus } from "./status.js";
|
|
|
9
9
|
export const createScopeTool = defineTool({
|
|
10
10
|
name: TOOL.applicationScopeCreate,
|
|
11
11
|
title: "Create scope",
|
|
12
|
-
description:
|
|
12
|
+
description: 'Create a SCOPE — a deploy target (e.g. dev/staging/main) — for an application; provisions real infrastructure asynchronously. Use it for "add a dev/staging/prod environment", "create a scope", "set up a new deploy target". Pass `name`; the scope `type` is inferred or auto-pinned when the org has one, else the form lists the org\'s types to pick from; `dimensions` default to the shape of an existing scope when the org uses them. Convergent: an existing scope of the same name is returned, never duplicated. Renders the app panel; once active, deploy to it with application_deployment_create scope:"<name>".',
|
|
13
13
|
// Convergent under retries (reuses the scope-by-name), so idempotent.
|
|
14
14
|
annotations: { destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
15
15
|
widget: "np-panel",
|
|
@@ -4,7 +4,7 @@ import { dashboardLink, linkLine, next, table } from "../md.js";
|
|
|
4
4
|
import { createServiceAction, getService, listAppServices, listDependencySpecs, provisionService, serviceCreateActionSpec, } from "../np/journey.js";
|
|
5
5
|
import { defineTool, fail, reply } from "../tool.js";
|
|
6
6
|
import { TOOL } from "../tool-names.js";
|
|
7
|
-
import { appArg, delays, requireApp, sleep } from "./shared.js";
|
|
7
|
+
import { appArg, delays, requireApp, resolveChoice, sleep } from "./shared.js";
|
|
8
8
|
const slug = (text) => text
|
|
9
9
|
.toLowerCase()
|
|
10
10
|
.replace(/[^a-z0-9]+/g, "-")
|
|
@@ -66,23 +66,23 @@ export const createServiceTool = defineTool({
|
|
|
66
66
|
return fail(translate("createService.noSpecs", { app: app.name }) +
|
|
67
67
|
linkLine(translate("md.dashboard"), dashboard));
|
|
68
68
|
}
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
:
|
|
74
|
-
|
|
75
|
-
if (
|
|
76
|
-
const
|
|
77
|
-
const
|
|
69
|
+
// Pre-select via the shared resolver (same rule as namespace/template): an exact spec id (form
|
|
70
|
+
// submit) or the model's `type` hint, matched against the catalog on name + category + sub-category.
|
|
71
|
+
// None / ambiguous → return the catalog (or the narrowed matches) to pick from — never an arbitrary one.
|
|
72
|
+
const spec = resolveChoice(specs, { id: args.specification_id, text: args.type }, {
|
|
73
|
+
haystack: (candidate) => `${candidate.name} ${candidate.category ?? ""} ${candidate.subCategory ?? ""}`,
|
|
74
|
+
});
|
|
75
|
+
if (!spec) {
|
|
76
|
+
const typed = args.type ? matchSpecs(specs, args.type) : [];
|
|
77
|
+
const list = typed.length > 1 ? typed : specs;
|
|
78
|
+
const heading = translate(typed.length > 1 ? "createService.matchAmbiguous" : "createService.pickSpec", {
|
|
78
79
|
app: app.name,
|
|
79
80
|
query: args.type ?? "",
|
|
80
81
|
count: specs.length,
|
|
81
82
|
});
|
|
82
|
-
const md = `${heading}\n\n${table([translate("header.name"), translate("createService.category"), translate("createService.provider")], list.map((
|
|
83
|
+
const md = `${heading}\n\n${table([translate("header.name"), translate("createService.category"), translate("createService.provider")], list.map((candidate) => [candidate.name, specCategory(candidate), candidate.provider ?? ""]))}${next(translate("createService.pickHint"))}`;
|
|
83
84
|
return reply(md, { mode: "pick-spec", app: `#${app.id}`, app_name: app.name, specs: list, dashboard });
|
|
84
85
|
}
|
|
85
|
-
const spec = matches[0];
|
|
86
86
|
const createAction = await serviceCreateActionSpec(context.np, spec.id);
|
|
87
87
|
const name = (args.name ?? spec.name).trim();
|
|
88
88
|
// Form-first: no explicit provision OR a required create-action param still missing → show the
|
|
@@ -19,7 +19,7 @@ const slug = (text) => text
|
|
|
19
19
|
export const deleteLinkTool = defineTool({
|
|
20
20
|
name: TOOL.applicationLinkDelete,
|
|
21
21
|
title: "Delete link",
|
|
22
|
-
description:
|
|
22
|
+
description: 'DELETE a link between a service and an application, removing the parameters it exported. Use it for "unlink/disconnect the <service> from <app>", "remove the link". Pass `link` (by name); call WITHOUT `confirm:true` first to open a confirm form (preview), the user confirms by submitting it. A credential link deprovisions its database user before the record is removed; a redeploy applies the parameter removal to the runtime. Destructive; nothing is removed without the submit.',
|
|
23
23
|
annotations: { destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
24
24
|
widget: "service-delete",
|
|
25
25
|
errorKey: "deleteEntity.linkErrorLabel",
|
|
@@ -13,7 +13,7 @@ import { appArg, requireApp } from "./shared.js";
|
|
|
13
13
|
export const deleteParamTool = defineTool({
|
|
14
14
|
name: TOOL.applicationParameterDelete,
|
|
15
15
|
title: "Delete parameter",
|
|
16
|
-
description:
|
|
16
|
+
description: 'DELETE an application configuration parameter — the whole variable and ALL its values. Use it for "delete/remove the <NAME> env var/parameter". Pass `name`; call WITHOUT `confirm:true` first to open a confirm form (preview), the user confirms by submitting it. Read-only parameters injected by a service link can\'t be deleted here — delete the link instead (application_link_delete). Applies on the next deploy. To CHANGE a value instead of removing it, use application_parameter_create.',
|
|
17
17
|
annotations: { destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
18
18
|
widget: "params",
|
|
19
19
|
errorKey: "deleteParam.errorLabel",
|
|
@@ -19,7 +19,7 @@ const slug = (text) => text
|
|
|
19
19
|
export const deleteServiceTool = defineTool({
|
|
20
20
|
name: TOOL.applicationServiceDelete,
|
|
21
21
|
title: "Delete service",
|
|
22
|
-
description:
|
|
22
|
+
description: 'DELETE a provisioned service and destroy its cloud resources. Use it for "delete/remove/tear down the <service>". Pass `service` (by name); call WITHOUT `confirm:true` first to open a confirm form showing what will be destroyed, the user confirms by submitting it. A service with active links can\'t be deleted — remove the links first with application_link_delete. Destructive and not reversible; nothing is destroyed without the submit.',
|
|
23
23
|
annotations: { destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
24
24
|
widget: "service-delete",
|
|
25
25
|
errorKey: "deleteEntity.serviceErrorLabel",
|
package/dist/tools/deploy.js
CHANGED
|
@@ -82,7 +82,7 @@ async function resolveRelease(context, applicationId, args) {
|
|
|
82
82
|
export const deployTool = defineTool({
|
|
83
83
|
name: TOOL.applicationDeploymentCreate,
|
|
84
84
|
title: "Deploy",
|
|
85
|
-
description: '
|
|
85
|
+
description: 'DEPLOY an application — provisions real infrastructure (starts a rollout), rendered as the live rollout panel. With no extra args it ships the latest code: picks the latest successful build, cuts the release for you (semver auto-bumped) if needed, and targets the app\'s only scope. Use it for "deploy", "ship it", "release to dev/prod". Narrow the target with `scope` (name or "environment=production"); pin an exact `release_id` or `version` (a known version rolls FORWARD or BACK to it); or cut from a specific `build_id`. For an app that splits traffic, FOLLOW with application_deployment_update to walk traffic to the new version; to undo, application_deployment_update action:"rollback". Convergent under retries: re-running returns the in-flight rollout for the same scope+release rather than creating a duplicate.',
|
|
86
86
|
annotations: { destructiveHint: false, openWorldHint: true },
|
|
87
87
|
widget: "np-panel",
|
|
88
88
|
errorKey: "deploy.errorLabel",
|
|
@@ -13,7 +13,7 @@ import { appArg, offsetArg, pageOf, requireApp } from "./shared.js";
|
|
|
13
13
|
export const deploymentsTool = defineTool({
|
|
14
14
|
name: TOOL.applicationDeploymentList,
|
|
15
15
|
title: "Deployments",
|
|
16
|
-
description:
|
|
16
|
+
description: 'List ONE application\'s deployments across all its scopes, rendered as a PANEL — which release landed on which scope, the rollout status, and age (a deployment group landing one release on several scopes shows as one row). Use it for "deploy history", "what was deployed when", "find the active rollout". Does NOT deploy or change traffic (use application_deployment_create / application_deployment_update) and shows no logs/metrics. To find the newest deployment ACROSS apps, gather headlessly with entity_list (entity:"deployment") and compute it. Read-only.',
|
|
17
17
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
18
18
|
widget: "deployments",
|
|
19
19
|
errorKey: "deploymentList.errorLabel",
|
package/dist/tools/entity-get.js
CHANGED
|
@@ -21,7 +21,7 @@ const ENTITIES = [
|
|
|
21
21
|
export const entityGetTool = defineTool({
|
|
22
22
|
name: TOOL.entityGet,
|
|
23
23
|
title: "Read an entity (data)",
|
|
24
|
-
description: 'Read
|
|
24
|
+
description: 'Read ONE entity by id as DATA — returns structuredContent and renders NO panel (no widget). Use it to (a) drill into a specific entity while navigating headlessly with entity_list, or (b) POLL a changing status WITHOUT spamming panels — loop entity_get entity:"build" id:<id> (or "deployment") until the status is final, instead of re-calling application_get each time. Pass `entity` + `id` (e.g. entity:"application" id:123). Entities: organization, account, namespace, application, scope, build, release, deployment. Use the rendered reads (application_get/…) when the USER wants to SEE it; parameters/services/links/approvals have their own tools.',
|
|
25
25
|
annotations: { readOnlyHint: true },
|
|
26
26
|
// No widget on purpose: data-only. See CLAUDE.md (the data-only read pattern).
|
|
27
27
|
errorKey: "entityGet.errorLabel",
|
|
@@ -58,7 +58,7 @@ async function listByNrn(context, entity, nrn) {
|
|
|
58
58
|
export const entityListTool = defineTool({
|
|
59
59
|
name: TOOL.entityList,
|
|
60
60
|
title: "List entities (data)",
|
|
61
|
-
description: '
|
|
61
|
+
description: 'List releases, builds, deployments, scopes, applications, namespaces, parameters, services, links or approvals as DATA — returns structuredContent and renders NO panel. THIS is the tool for AGGREGATING or COMPARING across many apps/scopes: "the latest release across all my apps", "which app has a failing build", "every scope on the newest build" — list each app\'s releases/builds with this (no panel per app), then compute the answer yourself. Prefer it over application_release_list/application_build_list/application_deployment_list whenever you are gathering to reason or to answer an aggregate — those panel reads are ONLY for showing ONE app\'s list to the user. Scoped to a parent: REST tree organization → account → namespace → application → {scope, build, release}; scope → deployment — pass `entity` plus the `parent_type`+`parent_id` it lives under (e.g. entity:"release" parent_type:"application" parent_id:123; listing `account` needs no parent). NRN-scoped resources (parameter, service, link, approval) live under an application or scope — pass parent_type:"application"|"scope" + parent_id (the NRN is resolved for you), or `nrn` directly. For one entity by id use entity_get.',
|
|
62
62
|
annotations: { readOnlyHint: true },
|
|
63
63
|
// No widget on purpose: data-only navigation. See CLAUDE.md (the data-only read pattern).
|
|
64
64
|
errorKey: "entityList.errorLabel",
|
package/dist/tools/find-apps.js
CHANGED
|
@@ -8,13 +8,16 @@ import { TOOL } from "../tool-names.js";
|
|
|
8
8
|
export const findAppsTool = defineTool({
|
|
9
9
|
name: TOOL.applicationList,
|
|
10
10
|
title: "Applications",
|
|
11
|
-
description:
|
|
11
|
+
description: 'Search applications across the organization by partial name and/or namespace, rendered as a list PANEL grouped by namespace. Fast (parallel + cached). When the user asks for the apps IN a namespace ("my apps in pablov", "list the payments-ns apps"), ALWAYS pass `namespace` so the search is filtered at the source — never list the whole org and then filter the result yourself (the org can have hundreds of apps; post-filtering in code is wrong). Namespace matching is lenient (case- and space-insensitive: "pablov" matches "Pablo V"). Use `query` for a partial app name. Use this when the repo isn\'t linked or the user names an app/namespace you don\'t know.',
|
|
12
12
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
13
13
|
widget: "find-apps",
|
|
14
14
|
errorKey: "findApps.errorLabel",
|
|
15
15
|
inputSchema: {
|
|
16
16
|
query: z.string().optional().describe("Partial app name (case-insensitive). Omit to list everything."),
|
|
17
|
-
namespace: z
|
|
17
|
+
namespace: z
|
|
18
|
+
.string()
|
|
19
|
+
.optional()
|
|
20
|
+
.describe('Limit to a namespace by name — pass this whenever the user scopes to a namespace. Matched leniently (case- and space-insensitive): "pablov" or "pablo v" both match "Pablo V".'),
|
|
18
21
|
limit: z.number().optional(),
|
|
19
22
|
},
|
|
20
23
|
async handler(args, context) {
|
package/dist/tools/logs.js
CHANGED
|
@@ -8,7 +8,7 @@ import { appArg, chooseScope, requireApp } from "./shared.js";
|
|
|
8
8
|
export const logsTool = defineTool({
|
|
9
9
|
name: TOOL.applicationLogList,
|
|
10
10
|
title: "Logs",
|
|
11
|
-
description:
|
|
11
|
+
description: 'Read ONE application\'s recent logs (per scope), rendered as a logs PANEL — the latest lines, newest last. Use it for "why is it failing?", "show me the logs", "what errored". Logs are per-scope: omit `scope` to use the only one (or be asked which); page older lines with `page_token`; narrow the window with `start_time`/`end_time` (ISO-8601) or cap with `lines`. Does NOT return metrics or deploy status — use application_metric_list or application_get. Read-only.',
|
|
12
12
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
13
13
|
widget: "logs",
|
|
14
14
|
errorKey: "logs.errorLabel",
|
package/dist/tools/metrics.js
CHANGED
|
@@ -8,7 +8,7 @@ import { appArg, chooseScope, requireApp } from "./shared.js";
|
|
|
8
8
|
export const metricsTool = defineTool({
|
|
9
9
|
name: TOOL.applicationMetricList,
|
|
10
10
|
title: "Performance metrics",
|
|
11
|
-
description:
|
|
11
|
+
description: 'The golden signals of ONE scope — throughput (rpm), response time, error rate, CPU, memory — with sparkline trends, rendered as a metrics PANEL. Use it for "how is it performing?", "is it healthy?", or to watch a scope during a rollout. Pick the `window` (1h/3h/24h/7d, default 3h); omit `scope` to use the only one. Does NOT return logs or deploy state — use application_log_list or application_get. Read-only.',
|
|
12
12
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
13
13
|
widget: "metrics",
|
|
14
14
|
errorKey: "metrics.errorLabel",
|
package/dist/tools/overview.js
CHANGED
|
@@ -14,7 +14,7 @@ const MAX_APPS = 30;
|
|
|
14
14
|
export const overviewTool = defineTool({
|
|
15
15
|
name: TOOL.organizationGet,
|
|
16
16
|
title: "Organization overview",
|
|
17
|
-
description:
|
|
17
|
+
description: 'A cross-application health digest for the WHOLE org, rendered as a panel: what\'s mid-rollout right now, and which scopes\' last deployment failed or rolled back. Use it for org-wide questions that name no app — "is anything broken?", "what\'s deploying?", "any failed rollouts?". Scans up to the first ~30 applications (truncation is reported, never silent). Does NOT show one app\'s full detail (use application_get), nor logs/metrics; to find the latest release or build ACROSS apps, gather headlessly with entity_list and compute it — don\'t call this. Read-only.',
|
|
18
18
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
19
19
|
widget: "overview",
|
|
20
20
|
errorKey: "overview.errorLabel",
|
package/dist/tools/params.js
CHANGED
|
@@ -24,7 +24,7 @@ function valueSummary(parameter, scopeNameById) {
|
|
|
24
24
|
export const paramsTool = defineTool({
|
|
25
25
|
name: TOOL.applicationParameterList,
|
|
26
26
|
title: "Parameters",
|
|
27
|
-
description:
|
|
27
|
+
description: 'List ONE application\'s configuration parameters and EVERY value each holds across scopes and dimensions (env vars / files; secret values are masked, never shown in plaintext), rendered as a PANEL. Use it for "what env vars does this app have", "show the config", "what\'s DATABASE_URL set to". Pass `scope` to have the PLATFORM resolve the single effective value per parameter for that scope (it collapses scope value › most-specific dimension match › app default — never reimplement that precedence yourself). Does NOT change anything — use application_parameter_create to add/update, application_parameter_delete to remove. Read-only.',
|
|
28
28
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
29
29
|
widget: "params",
|
|
30
30
|
errorKey: "params.errorLabel",
|
package/dist/tools/playbook.js
CHANGED
|
@@ -14,7 +14,7 @@ const playbookBody = (markdown) => markdown.replace(/^---\n[\s\S]*?\n---\n?/, ""
|
|
|
14
14
|
export const playbookGetTool = defineTool({
|
|
15
15
|
name: TOOL.playbookGet,
|
|
16
16
|
title: "Operating playbooks",
|
|
17
|
-
description:
|
|
17
|
+
description: 'Read a nullplatform operating PLAYBOOK — model-facing methodology for RISKY or MULTI-STEP work: deploying-safely (canary steps + metric gates for a production rollout), incident-response (triage when something is broken), configuring-safely (changing parameters or secrets carefully). Renders NO panel — prose for YOU to consult, NOT a step you must run before every action. Reach for it only when you genuinely want the grounded procedure for a non-trivial operation; for a direct, simple command ("deploy", "set X", "roll back") just use that action\'s tool — do not detour here first. Call with no `name` to list the catalog (also in the server instructions).',
|
|
18
18
|
annotations: { readOnlyHint: true },
|
|
19
19
|
// No widget: a playbook is model-facing methodology prose, not an interactive entity — it
|
|
20
20
|
// renders as text (the markdown below) for the model to read and act on. See CLAUDE.md.
|
package/dist/tools/releases.js
CHANGED
|
@@ -12,7 +12,7 @@ import { appArg, offsetArg, pageOf, requireApp } from "./shared.js";
|
|
|
12
12
|
export const releasesTool = defineTool({
|
|
13
13
|
name: TOOL.applicationReleaseList,
|
|
14
14
|
title: "Releases",
|
|
15
|
-
description: "List
|
|
15
|
+
description: "List ONE application's releases as a PANEL for the user — semver, status (active/deprecated/unstable/failed), the build each pins, and age. Use to pick a release to deploy or promote, or to see what's shippable. Do NOT call this once per app to compare across apps (it renders a panel each time): for an AGGREGATE like 'the latest release across all my apps' use entity_list (entity:\"release\"), gather headlessly, and compute the answer.",
|
|
16
16
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
17
17
|
widget: "releases",
|
|
18
18
|
errorKey: "releaseList.errorLabel",
|
package/dist/tools/services.js
CHANGED
|
@@ -13,7 +13,7 @@ import { appArg, requireApp } from "./shared.js";
|
|
|
13
13
|
export const servicesTool = defineTool({
|
|
14
14
|
name: TOOL.applicationServiceList,
|
|
15
15
|
title: "Services & dependencies",
|
|
16
|
-
description:
|
|
16
|
+
description: 'List the dependency services (databases, queues, caches…) attached to ONE application, plus the catalog of dependency types available to provision, rendered as a PANEL. Use it for "what databases/queues does this app use", "show its dependencies", "what can I provision". To provision a new dependency use application_service_create; to connect one to a scope use application_link_create. Read-only — it does not provision or link anything itself.',
|
|
17
17
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
18
18
|
widget: "services",
|
|
19
19
|
errorKey: "services.errorLabel",
|
package/dist/tools/set-params.js
CHANGED
|
@@ -8,7 +8,7 @@ import { appArg, pickScope, requireApp } from "./shared.js";
|
|
|
8
8
|
export const setParamsTool = defineTool({
|
|
9
9
|
name: TOOL.applicationParameterCreate,
|
|
10
10
|
title: "Set parameters",
|
|
11
|
-
description:
|
|
11
|
+
description: 'Create or UPDATE application configuration parameters — environment variables or files, secrets supported (stored masked, updatable; NOT write-once). A parameter is a value-SET: one definition holds many values keyed by target. Target a value at the APPLICATION (default — every scope), BY DIMENSION (e.g. environment=production — applies to any matching scope), or pinned to ONE scope (dimensions and a scope are mutually exclusive). Use it for "set DATABASE_URL", "add an env var", "change a secret", "set a prod-only value". Values apply on the NEXT deploy. Renders the parameters panel after applying. Convergent: reuses the existing definition and updates in place rather than duplicating. View current values with application_parameter_list; remove with application_parameter_delete.',
|
|
12
12
|
annotations: { destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
13
13
|
widget: "params",
|
|
14
14
|
errorKey: "setParams.errorLabel",
|
package/dist/tools/shared.js
CHANGED
|
@@ -75,6 +75,49 @@ export async function requireApp(context, args) {
|
|
|
75
75
|
}
|
|
76
76
|
return { out: fail(notFoundMessage(resolution), { not_found: true }) };
|
|
77
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* The ONE pre-selection rule for every create/fill form selector (namespace, template, service spec,
|
|
80
|
+
* scope type…). It turns the model's soft inference — an explicit id, or the free-text stack/type it
|
|
81
|
+
* reasoned out of the conversation — into a concrete option, or `undefined` when it's a genuine
|
|
82
|
+
* unhinted choice. It NEVER returns an arbitrary first entry: a wrong pre-selection is worse than an
|
|
83
|
+
* honest blank. Division of labour (see CLAUDE.md): the MODEL infers (it holds the context) and passes
|
|
84
|
+
* the hint as a tool arg; the TOOL resolves it here; the WIDGET only mirrors the result.
|
|
85
|
+
*
|
|
86
|
+
* Order, most to least certain: explicit id → exact name → every query token present in the option's
|
|
87
|
+
* haystack (fuzzy "sql database") → an option name contained in the hint ("Golang Hexagonal - API" ⊃
|
|
88
|
+
* the "golang-hexagonal" template) → with `pinOnly`, the sole candidate. `haystack` widens the
|
|
89
|
+
* searchable text per field (name + tags, name + category…); it defaults to the option's name.
|
|
90
|
+
*/
|
|
91
|
+
export function resolveChoice(options, hint, settings = {}) {
|
|
92
|
+
if (hint.id != null) {
|
|
93
|
+
const byId = options.find((option) => option.id === hint.id);
|
|
94
|
+
if (byId)
|
|
95
|
+
return byId;
|
|
96
|
+
}
|
|
97
|
+
const query = hint.text?.trim().toLowerCase();
|
|
98
|
+
if (query) {
|
|
99
|
+
const haystackOf = settings.haystack ?? ((option) => option.name);
|
|
100
|
+
const exact = options.filter((option) => option.name.trim().toLowerCase() === query);
|
|
101
|
+
if (exact.length === 1)
|
|
102
|
+
return exact[0];
|
|
103
|
+
const tokens = query.split(/\s+/).filter(Boolean);
|
|
104
|
+
const tokenMatch = options.filter((option) => {
|
|
105
|
+
const haystack = haystackOf(option).toLowerCase();
|
|
106
|
+
return tokens.every((token) => haystack.includes(token));
|
|
107
|
+
});
|
|
108
|
+
if (tokenMatch.length === 1)
|
|
109
|
+
return tokenMatch[0];
|
|
110
|
+
const contained = options.filter((option) => {
|
|
111
|
+
const name = option.name.trim().toLowerCase();
|
|
112
|
+
return name.length >= 2 && query.includes(name);
|
|
113
|
+
});
|
|
114
|
+
if (contained.length === 1)
|
|
115
|
+
return contained[0];
|
|
116
|
+
}
|
|
117
|
+
if (settings.pinOnly && options.length === 1)
|
|
118
|
+
return options[0];
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
78
121
|
/**
|
|
79
122
|
* Match scopes by name OR by dimensions: "dev" (name substring), "production"
|
|
80
123
|
* (dimension value substring), "environment=production" (exact key=value).
|
package/dist/tools/status.js
CHANGED
|
@@ -41,7 +41,7 @@ export async function buildAppStatus(context, app) {
|
|
|
41
41
|
export const statusTool = defineTool({
|
|
42
42
|
name: TOOL.applicationGet,
|
|
43
43
|
title: "Application status",
|
|
44
|
-
description:
|
|
44
|
+
description: 'THE place to start for ONE application\'s live status — renders a status PANEL: each scope with what\'s live on it (release + traffic), the latest build, the latest release, and the single obvious next action. Use it for "what\'s the status of my app", "what\'s deployed where", "is it healthy" — a STATUS question. For a DIRECT action ("deploy", "send traffic", "show logs/metrics") reach for that action\'s own tool instead of starting here; only read status first when you genuinely need to see state before acting. Call with NO arguments inside a git repo to use the linked app; pass `app` (name or "#id") for a different one; pass `deployment:<id>` to watch one rollout\'s live detail. Does NOT return logs, metrics, parameters, or a build\'s assets — use application_log_list / application_metric_list / application_parameter_list / application_build_list for those. Do NOT loop this to WAIT for a build or deployment to finish: it repaints the whole panel every call (stacking duplicate panels) — poll headlessly with entity_get (entity:"build" or "deployment", id:<id>) instead, and show this panel once at the end.',
|
|
45
45
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
46
46
|
widget: "np-panel",
|
|
47
47
|
errorKey: "status.errorLabel",
|
package/dist/tools/traffic.js
CHANGED
|
@@ -9,7 +9,7 @@ import { appArg, delays, requireApp, sleep } from "./shared.js";
|
|
|
9
9
|
export const trafficTool = defineTool({
|
|
10
10
|
name: TOOL.applicationDeploymentUpdate,
|
|
11
11
|
title: "Update traffic",
|
|
12
|
-
description: 'Drive an
|
|
12
|
+
description: 'Drive an IN-FLIGHT rollout — the step AFTER application_deployment_create. Call it DIRECTLY with `percent` (snaps to 0,1,5,10,25,50,75,90,95,99,100) to move traffic, action:"finalize" to retire the old version, or action:"rollback" to return traffic to the previous version (the safety hatch). You do NOT need to read status or look up a deployment id first — it finds the app\'s single active rollout automatically (pass `deployment_id` only when several are in flight at once). Use it for "send 25% traffic", "promote to 100%", "finalize", "roll back". Renders the live rollout panel. Does NOT create a deployment — use application_deployment_create first; this only steers one that already exists.',
|
|
13
13
|
annotations: { destructiveHint: true, openWorldHint: true },
|
|
14
14
|
widget: "np-panel",
|
|
15
15
|
errorKey: "traffic.errorLabel",
|