@nullplatform/mcp 0.1.12 → 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/i18n.js +6 -0
- package/dist/np/context.js +6 -2
- package/dist/np/journey.js +15 -0
- package/dist/surfaces/developer.js +10 -1
- package/dist/tool-names.js +2 -0
- 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 +36 -0
- package/dist/tools/entity-list.js +99 -0
- package/dist/tools/find-apps.js +5 -2
- package/dist/tools/index.js +4 -0
- 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 +10 -2
- package/widgets-dist/builds.html +10 -2
- package/widgets-dist/create-app.html +18 -10
- package/widgets-dist/deployments.html +10 -2
- package/widgets-dist/find-apps.html +10 -2
- package/widgets-dist/logs.html +10 -2
- package/widgets-dist/manifest.json +16 -16
- package/widgets-dist/metrics.html +10 -2
- package/widgets-dist/np-panel.html +10 -2
- package/widgets-dist/overview.html +19 -11
- package/widgets-dist/params.html +22 -14
- package/widgets-dist/releases.html +10 -2
- package/widgets-dist/service-action.html +9 -1
- package/widgets-dist/service-create.html +10 -2
- package/widgets-dist/service-delete.html +9 -1
- package/widgets-dist/service-link.html +10 -2
- package/widgets-dist/services.html +15 -7
package/dist/i18n.js
CHANGED
|
@@ -354,6 +354,8 @@ const english = {
|
|
|
354
354
|
"createApp.errorLabel": "Couldn't create the application",
|
|
355
355
|
// — playbook_get tool —
|
|
356
356
|
"playbook.errorLabel": "Couldn't load the playbook",
|
|
357
|
+
"entityList.errorLabel": "Couldn't list that",
|
|
358
|
+
"entityGet.errorLabel": "Couldn't read that",
|
|
357
359
|
// — application_scope_create tool —
|
|
358
360
|
"createScope.whichType": "Which scope type for **{name}**?",
|
|
359
361
|
"createScope.typeHint": "name a scope type for **{name}** to create it.",
|
|
@@ -720,6 +722,8 @@ const spanish = {
|
|
|
720
722
|
"createApp.errorLabel": "No pude crear la aplicación",
|
|
721
723
|
// — playbook_get tool —
|
|
722
724
|
"playbook.errorLabel": "No pude cargar el playbook",
|
|
725
|
+
"entityList.errorLabel": "No pude listar eso",
|
|
726
|
+
"entityGet.errorLabel": "No pude leer eso",
|
|
723
727
|
"createScope.whichType": "¿Qué tipo de scope para **{name}**?",
|
|
724
728
|
"createScope.typeHint": "nombrá un tipo de scope para **{name}** para crearlo.",
|
|
725
729
|
"createScope.noTypes": 'No hay tipos de scope disponibles — pasá type explícito (p. ej. type:"web_pool").',
|
|
@@ -1101,6 +1105,8 @@ const portuguese = {
|
|
|
1101
1105
|
"createApp.errorLabel": "Não foi possível criar a aplicação",
|
|
1102
1106
|
// — playbook_get tool —
|
|
1103
1107
|
"playbook.errorLabel": "Não foi possível carregar o playbook",
|
|
1108
|
+
"entityList.errorLabel": "Não foi possível listar isso",
|
|
1109
|
+
"entityGet.errorLabel": "Não foi possível ler isso",
|
|
1104
1110
|
// — application_scope_create tool —
|
|
1105
1111
|
"createScope.whichType": "Qual tipo de scope para **{name}**?",
|
|
1106
1112
|
"createScope.typeHint": "informe um tipo de scope para **{name}** para criá-lo.",
|
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
|
package/dist/np/journey.js
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic entity navigation. The platform's REST surface is uniform — every entity in the core tree
|
|
3
|
+
* lists as `GET /<entity>?<parent>_id=<id>` (returning `{results}`) and reads as `GET /<entity>/<id>`.
|
|
4
|
+
* One pair of accessors therefore resolves the whole hierarchy (organization → account → namespace →
|
|
5
|
+
* application → scope → deployment, plus build/release under application) without a per-entity function.
|
|
6
|
+
* (NRN-scoped resources — parameters/services/links/approvals — use a different addressing scheme and
|
|
7
|
+
* keep their own typed functions.) This is the typed seam the generic read tools call.
|
|
8
|
+
*/
|
|
9
|
+
export async function listEntity(np, entity, query) {
|
|
10
|
+
const page = await np.get(`/${entity}`, query);
|
|
11
|
+
return Array.isArray(page) ? page : (page.results ?? []);
|
|
12
|
+
}
|
|
13
|
+
export async function getEntity(np, entity, id) {
|
|
14
|
+
return np.get(`/${entity}/${id}`);
|
|
15
|
+
}
|
|
1
16
|
/** Traffic percentages the platform accepts (same marks the dashboard slider snaps to). */
|
|
2
17
|
export const TRAFFIC_MARKS = [0, 1, 5, 10, 25, 50, 75, 90, 95, 99, 100];
|
|
3
18
|
export const snapTraffic = (percent) => TRAFFIC_MARKS.reduce((closest, mark) => (Math.abs(mark - percent) < Math.abs(closest - percent) ? mark : closest), 0);
|
|
@@ -14,7 +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.
|
|
21
|
+
|
|
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.
|
|
18
27
|
|
|
19
28
|
Typical flows:
|
|
20
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/tool-names.js
CHANGED
|
@@ -30,5 +30,7 @@ export const TOOL = {
|
|
|
30
30
|
applicationParameterDelete: "application_parameter_delete",
|
|
31
31
|
applicationApprovalList: "application_approval_list",
|
|
32
32
|
organizationGet: "organization_get",
|
|
33
|
+
entityList: "entity_list",
|
|
34
|
+
entityGet: "entity_get",
|
|
33
35
|
playbookGet: "playbook_get",
|
|
34
36
|
};
|
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",
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getEntity } from "../np/journey.js";
|
|
3
|
+
import { defineTool, reply } from "../tool.js";
|
|
4
|
+
import { TOOL } from "../tool-names.js";
|
|
5
|
+
/**
|
|
6
|
+
* Generic read-only, DATA-only GET — NO widget, so nothing renders. Reads ONE core entity by id
|
|
7
|
+
* (`GET /<entity>/<id>`), the drill-in companion to `entity_list`. The model uses it to inspect a
|
|
8
|
+
* specific node while navigating the tree, without putting a panel on the user's screen. The rendered
|
|
9
|
+
* reads (application_get/…) stay for when the user wants to SEE it. See CLAUDE.md (data-only read).
|
|
10
|
+
*/
|
|
11
|
+
const ENTITIES = [
|
|
12
|
+
"organization",
|
|
13
|
+
"account",
|
|
14
|
+
"namespace",
|
|
15
|
+
"application",
|
|
16
|
+
"scope",
|
|
17
|
+
"build",
|
|
18
|
+
"release",
|
|
19
|
+
"deployment",
|
|
20
|
+
];
|
|
21
|
+
export const entityGetTool = defineTool({
|
|
22
|
+
name: TOOL.entityGet,
|
|
23
|
+
title: "Read an entity (data)",
|
|
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
|
+
annotations: { readOnlyHint: true },
|
|
26
|
+
// No widget on purpose: data-only. See CLAUDE.md (the data-only read pattern).
|
|
27
|
+
errorKey: "entityGet.errorLabel",
|
|
28
|
+
inputSchema: {
|
|
29
|
+
entity: z.enum(ENTITIES).describe("Which entity type to read."),
|
|
30
|
+
id: z.number().describe("The entity's id."),
|
|
31
|
+
},
|
|
32
|
+
async handler(args, context) {
|
|
33
|
+
const result = await getEntity(context.np, args.entity, args.id);
|
|
34
|
+
return reply(`${args.entity} #${args.id}.`, { entity: args.entity, result });
|
|
35
|
+
},
|
|
36
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getApplication, getScope, listApprovals, listEntity, listLinks, listParameters, listServices, } from "../np/journey.js";
|
|
3
|
+
import { defineTool, fail, reply } from "../tool.js";
|
|
4
|
+
import { TOOL } from "../tool-names.js";
|
|
5
|
+
/**
|
|
6
|
+
* Generic read-only, DATA-only LIST — NO widget, so nothing renders. One tool navigates the whole
|
|
7
|
+
* entity tree: pick an `entity` and the `parent_type`/`parent_id` it lives under. Two addressing
|
|
8
|
+
* schemes hide behind one uniform interface — the REST tree lists as `GET /<entity>?<parent>_id=<id>`,
|
|
9
|
+
* and the NRN-scoped resources (parameter/service/link/approval) list by the parent's NRN (resolved
|
|
10
|
+
* from `parent_id`, or passed directly as `nrn`). Each dispatches to its existing typed function in
|
|
11
|
+
* journey.ts (tools never touch NpClient directly). The model uses it to traverse the org and gather
|
|
12
|
+
* context without a panel; pair it with `entity_get` to drill into one. This is the data-only read
|
|
13
|
+
* pattern (see CLAUDE.md).
|
|
14
|
+
*/
|
|
15
|
+
/** REST-addressed collections — listed by `<parent_type>_id`. */
|
|
16
|
+
const REST_ENTITIES = [
|
|
17
|
+
"account",
|
|
18
|
+
"namespace",
|
|
19
|
+
"application",
|
|
20
|
+
"scope",
|
|
21
|
+
"build",
|
|
22
|
+
"release",
|
|
23
|
+
"deployment",
|
|
24
|
+
];
|
|
25
|
+
/** NRN-addressed collections — listed by the parent's NRN (resolved from parent_id, or given as nrn). */
|
|
26
|
+
const NRN_ENTITIES = ["parameter", "service", "link", "approval"];
|
|
27
|
+
const ENTITIES = [...REST_ENTITIES, ...NRN_ENTITIES];
|
|
28
|
+
/** The parents a collection can hang under — its id (REST) or its NRN (nrn-scoped) scopes the list. */
|
|
29
|
+
const PARENTS = ["organization", "account", "namespace", "application", "scope"];
|
|
30
|
+
const NRN_ENTITY_SET = new Set(NRN_ENTITIES);
|
|
31
|
+
/** Resolve the NRN that scopes an nrn-addressed collection: the model's `nrn`, or its parent's. */
|
|
32
|
+
async function resolveNrn(context, args) {
|
|
33
|
+
if (args.nrn)
|
|
34
|
+
return args.nrn;
|
|
35
|
+
if (args.parent_id === undefined)
|
|
36
|
+
return undefined;
|
|
37
|
+
if (args.parent_type === "application")
|
|
38
|
+
return (await getApplication(context.np, args.parent_id)).nrn;
|
|
39
|
+
if (args.parent_type === "scope")
|
|
40
|
+
return (await getScope(context.np, args.parent_id)).nrn;
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
/** List an nrn-scoped resource via its existing typed function (a different addressing scheme to REST). */
|
|
44
|
+
async function listByNrn(context, entity, nrn) {
|
|
45
|
+
switch (entity) {
|
|
46
|
+
case "parameter":
|
|
47
|
+
return (await listParameters(context.np, nrn)) ?? [];
|
|
48
|
+
case "service":
|
|
49
|
+
return listServices(context.bff, nrn);
|
|
50
|
+
case "link":
|
|
51
|
+
return listLinks(context.np, nrn);
|
|
52
|
+
case "approval":
|
|
53
|
+
return listApprovals(context.bff, nrn);
|
|
54
|
+
default:
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export const entityListTool = defineTool({
|
|
59
|
+
name: TOOL.entityList,
|
|
60
|
+
title: "List entities (data)",
|
|
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
|
+
annotations: { readOnlyHint: true },
|
|
63
|
+
// No widget on purpose: data-only navigation. See CLAUDE.md (the data-only read pattern).
|
|
64
|
+
errorKey: "entityList.errorLabel",
|
|
65
|
+
inputSchema: {
|
|
66
|
+
entity: z
|
|
67
|
+
.enum(ENTITIES)
|
|
68
|
+
.describe("Collection to list. REST tree: account, namespace, application, scope, build, release, deployment. NRN-scoped: parameter, service, link, approval."),
|
|
69
|
+
parent_type: z
|
|
70
|
+
.enum(PARENTS)
|
|
71
|
+
.optional()
|
|
72
|
+
.describe("The parent the collection lives under. REST: its `<parent_type>_id` scopes the list (required except `account`). NRN-scoped: application|scope, used to resolve the NRN."),
|
|
73
|
+
parent_id: z.number().optional().describe("The parent entity's id."),
|
|
74
|
+
nrn: z
|
|
75
|
+
.string()
|
|
76
|
+
.optional()
|
|
77
|
+
.describe("For nrn-scoped entities (parameter/service/link/approval): the scoping NRN directly, if you have it; otherwise pass parent_type + parent_id and it's resolved for you."),
|
|
78
|
+
limit: z.number().optional().describe("Max rows (default 100) — REST collections only."),
|
|
79
|
+
},
|
|
80
|
+
async handler(args, context) {
|
|
81
|
+
if (NRN_ENTITY_SET.has(args.entity)) {
|
|
82
|
+
const nrn = await resolveNrn(context, args);
|
|
83
|
+
if (!nrn) {
|
|
84
|
+
return fail(`Listing ${args.entity} needs an NRN — pass nrn, or parent_type:"application"|"scope" + parent_id.`);
|
|
85
|
+
}
|
|
86
|
+
const nrnResults = await listByNrn(context, args.entity, nrn);
|
|
87
|
+
return reply(`${nrnResults.length} ${args.entity}(s).`, { entity: args.entity, results: nrnResults });
|
|
88
|
+
}
|
|
89
|
+
const query = { limit: args.limit ?? 100 };
|
|
90
|
+
if (args.parent_type && args.parent_id !== undefined) {
|
|
91
|
+
query[`${args.parent_type}_id`] = args.parent_id;
|
|
92
|
+
}
|
|
93
|
+
else if (args.entity === "account") {
|
|
94
|
+
query.organization_id = (await context.org.getSkeleton()).orgId;
|
|
95
|
+
}
|
|
96
|
+
const results = await listEntity(context.np, args.entity, query);
|
|
97
|
+
return reply(`${results.length} ${args.entity}(s).`, { entity: args.entity, results });
|
|
98
|
+
},
|
|
99
|
+
});
|
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/index.js
CHANGED
|
@@ -10,6 +10,8 @@ import { deleteParamTool } from "./delete-param.js";
|
|
|
10
10
|
import { deleteServiceTool } from "./delete-service.js";
|
|
11
11
|
import { deployTool } from "./deploy.js";
|
|
12
12
|
import { deploymentsTool } from "./deployments.js";
|
|
13
|
+
import { entityGetTool } from "./entity-get.js";
|
|
14
|
+
import { entityListTool } from "./entity-list.js";
|
|
13
15
|
import { findAppsTool } from "./find-apps.js";
|
|
14
16
|
import { logsTool } from "./logs.js";
|
|
15
17
|
import { metricsTool } from "./metrics.js";
|
|
@@ -45,6 +47,8 @@ export const tools = [
|
|
|
45
47
|
createAppTool,
|
|
46
48
|
createScopeTool,
|
|
47
49
|
approvalsTool,
|
|
50
|
+
entityListTool,
|
|
51
|
+
entityGetTool,
|
|
48
52
|
servicesTool,
|
|
49
53
|
createServiceTool,
|
|
50
54
|
updateServiceTool,
|
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",
|