@nullplatform/mcp 0.1.2 → 0.1.4
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 +28 -0
- package/dist/np/context.js +39 -28
- package/dist/np/journey.js +43 -8
- package/dist/surfaces/developer.js +3 -1
- package/dist/tool-names.js +2 -0
- package/dist/tools/builds.js +15 -5
- package/dist/tools/create-release.js +1 -1
- package/dist/tools/deploy.js +1 -1
- package/dist/tools/deployments.js +81 -0
- package/dist/tools/find-apps.js +1 -1
- package/dist/tools/index.js +4 -0
- package/dist/tools/logs.js +1 -1
- package/dist/tools/params.js +10 -5
- package/dist/tools/playbook.js +2 -2
- package/dist/tools/releases.js +59 -0
- package/dist/tools/shared.js +13 -0
- package/dist/tools/status.js +2 -2
- package/dist/tools/traffic.js +2 -2
- package/dist/ui.js +3 -0
- package/package.json +1 -1
- package/skills/configuring-safely/SKILL.md +50 -0
- package/skills/deploying-safely/SKILL.md +6 -3
- package/skills/incident-response/SKILL.md +4 -1
- package/skills/managing-environments/SKILL.md +53 -0
- package/skills/platform-conventions/SKILL.md +19 -9
- package/skills/promoting-across-environments/SKILL.md +51 -0
- package/skills/tracing-a-change/SKILL.md +30 -0
- package/skills/understand-a-service/SKILL.md +50 -0
- package/skills/working-from-a-ticket/SKILL.md +47 -0
- package/widgets-dist/builds.html +866 -0
- package/widgets-dist/create-app.html +51 -14
- package/widgets-dist/deployments.html +868 -0
- package/widgets-dist/find-apps.html +64 -27
- package/widgets-dist/logs.html +53 -16
- package/widgets-dist/manifest.json +9 -6
- package/widgets-dist/metrics.html +52 -15
- package/widgets-dist/np-panel.html +54 -17
- package/widgets-dist/params.html +52 -15
- package/widgets-dist/releases.html +868 -0
package/dist/i18n.js
CHANGED
|
@@ -51,6 +51,9 @@ const english = {
|
|
|
51
51
|
"header.live": "Live",
|
|
52
52
|
"header.traffic": "Traffic",
|
|
53
53
|
"header.when": "When",
|
|
54
|
+
"header.version": "Version",
|
|
55
|
+
"header.build": "Build",
|
|
56
|
+
"header.release": "Release",
|
|
54
57
|
// — markdown design language —
|
|
55
58
|
"md.next": "Next",
|
|
56
59
|
"md.none": "_none_",
|
|
@@ -160,6 +163,7 @@ const english = {
|
|
|
160
163
|
"builds.commit": "Commit",
|
|
161
164
|
"builds.released": "Released",
|
|
162
165
|
"builds.none": "**{app}** has no builds yet.",
|
|
166
|
+
"builds.noneForCommit": "No build in **{app}** for commit `{commit}` — it may not have built yet, or the commit isn't on a built branch.",
|
|
163
167
|
"builds.noneHint": "push a commit so CI produces one, then `application_deployment_create`.",
|
|
164
168
|
"builds.deployHint": "`application_deployment_create build_id:{build}` ships it (cuts the release for you).",
|
|
165
169
|
"builds.waiting": "build #{build} is still running — `application_build_list` again in a moment to check.",
|
|
@@ -167,6 +171,16 @@ const english = {
|
|
|
167
171
|
"builds.assetsTitle": "Assets of build #{build}",
|
|
168
172
|
"builds.noAssets": "Build #{build} has no assets (it may still be building).",
|
|
169
173
|
"builds.errorLabel": "Couldn't list builds",
|
|
174
|
+
"releaseList.title": "**{app}** · {count} release(s)",
|
|
175
|
+
"releaseList.none": "**{app}** has no releases yet.",
|
|
176
|
+
"releaseList.noneHint": "cut one from a successful build with `application_release_create`, or just `application_deployment_create`.",
|
|
177
|
+
"releaseList.deployHint": 'deploy an active release with `application_deployment_create version:"x.y.z"`.',
|
|
178
|
+
"releaseList.errorLabel": "Couldn't list releases",
|
|
179
|
+
"deploymentList.title": "**{app}** · {count} deployment(s)",
|
|
180
|
+
"deploymentList.none": "**{app}** has no deployments yet.",
|
|
181
|
+
"deploymentList.group": "{count} scopes (group)",
|
|
182
|
+
"deploymentList.hint": "`application_get` for the live picture, or `application_deployment_update` to drive a rollout.",
|
|
183
|
+
"deploymentList.errorLabel": "Couldn't list deployments",
|
|
170
184
|
// — organization_get tool —
|
|
171
185
|
"overview.title": "Organization overview · {count} application(s) scanned",
|
|
172
186
|
"overview.truncated": '_(showing the first {count} — narrow with `organization_get query:"..."`)_',
|
|
@@ -324,6 +338,9 @@ const spanish = {
|
|
|
324
338
|
"header.live": "En vivo",
|
|
325
339
|
"header.traffic": "Tráfico",
|
|
326
340
|
"header.when": "Cuándo",
|
|
341
|
+
"header.version": "Versión",
|
|
342
|
+
"header.build": "Build",
|
|
343
|
+
"header.release": "Release",
|
|
327
344
|
"md.next": "Siguiente",
|
|
328
345
|
"md.none": "_ninguno_",
|
|
329
346
|
"md.justNow": "recién",
|
|
@@ -423,6 +440,7 @@ const spanish = {
|
|
|
423
440
|
"builds.commit": "Commit",
|
|
424
441
|
"builds.released": "Released",
|
|
425
442
|
"builds.none": "**{app}** todavía no tiene builds.",
|
|
443
|
+
"builds.noneForCommit": "Ningún build en **{app}** para el commit `{commit}` — puede que todavía no haya buildeado, o que el commit no esté en una branch que buildea.",
|
|
426
444
|
"builds.noneHint": "pusheá un commit para que CI genere uno, después `application_deployment_create`.",
|
|
427
445
|
"builds.deployHint": "`application_deployment_create build_id:{build}` lo publica (crea la release por vos).",
|
|
428
446
|
"builds.waiting": "el build #{build} sigue corriendo — `application_build_list` de nuevo en un momento para chequear.",
|
|
@@ -430,6 +448,16 @@ const spanish = {
|
|
|
430
448
|
"builds.assetsTitle": "Assets del build #{build}",
|
|
431
449
|
"builds.noAssets": "El build #{build} no tiene assets (puede estar compilando todavía).",
|
|
432
450
|
"builds.errorLabel": "No pude listar los builds",
|
|
451
|
+
"releaseList.title": "**{app}** · {count} release(s)",
|
|
452
|
+
"releaseList.none": "**{app}** todavía no tiene releases.",
|
|
453
|
+
"releaseList.noneHint": "cortá uno desde un build exitoso con `application_release_create`, o directamente `application_deployment_create`.",
|
|
454
|
+
"releaseList.deployHint": 'deployá un release activo con `application_deployment_create version:"x.y.z"`.',
|
|
455
|
+
"releaseList.errorLabel": "No pude listar los releases",
|
|
456
|
+
"deploymentList.title": "**{app}** · {count} deployment(s)",
|
|
457
|
+
"deploymentList.none": "**{app}** todavía no tiene deployments.",
|
|
458
|
+
"deploymentList.group": "{count} scopes (grupo)",
|
|
459
|
+
"deploymentList.hint": "`application_get` para la foto en vivo, o `application_deployment_update` para conducir un rollout.",
|
|
460
|
+
"deploymentList.errorLabel": "No pude listar los deployments",
|
|
433
461
|
// — organization_get tool —
|
|
434
462
|
"overview.title": "Panorama de la organización · {count} aplicación(es) escaneada(s)",
|
|
435
463
|
"overview.truncated": '_(muestro las primeras {count} — acotá con `organization_get query:"..."`)_',
|
package/dist/np/context.js
CHANGED
|
@@ -12,10 +12,15 @@ export function baseRepoUrl(provider, prefix) {
|
|
|
12
12
|
: "https://github.com";
|
|
13
13
|
return `${host}/${prefix}`;
|
|
14
14
|
}
|
|
15
|
-
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
|
|
15
|
+
/** Org-wide app discovery must fan out across namespaces: the public `/application` endpoint is
|
|
16
|
+
* namespace-scoped, with no org-wide list and no name search — so the name filter is client-side
|
|
17
|
+
* by necessity. We page each namespace to completion and bound the AGGREGATE at this cap, which
|
|
18
|
+
* is generous and lift-able via NP_MAX_APPS, reporting truncation past it instead of silently
|
|
19
|
+
* hiding a large org (the old code capped the whole org at 200 with a flat `.slice`). */
|
|
20
|
+
const MAX_APPS = Math.max(1, Number(process.env.NP_MAX_APPS) || 1000);
|
|
21
|
+
export const DEFAULT_APP_LIMIT = MAX_APPS;
|
|
22
|
+
/** Safety ceiling when paging the account/namespace skeleton (both rarely exceed one page). */
|
|
23
|
+
const SKELETON_CAP = 2000;
|
|
19
24
|
/** Concurrency-limited parallel map — fast fan-out without hammering the API. */
|
|
20
25
|
export async function pmap(items, mapItem, limit = 12) {
|
|
21
26
|
const results = new Array(items.length);
|
|
@@ -29,6 +34,20 @@ export async function pmap(items, mapItem, limit = 12) {
|
|
|
29
34
|
await Promise.all(Array.from({ length: Math.min(limit, items.length || 1) }, worker));
|
|
30
35
|
return results;
|
|
31
36
|
}
|
|
37
|
+
/** Page an offset-based list endpoint to completion, bounded by `max` items. No list response
|
|
38
|
+
* carries a total, so the boundary is the standard offset heuristic: a page shorter than the
|
|
39
|
+
* requested limit is the last one. */
|
|
40
|
+
async function fetchPaged(fetchPage, max, pageSize = 200) {
|
|
41
|
+
const rows = [];
|
|
42
|
+
for (let offset = 0; offset < max; offset += pageSize) {
|
|
43
|
+
const limit = Math.min(pageSize, max - offset);
|
|
44
|
+
const page = await fetchPage(offset, limit);
|
|
45
|
+
rows.push(...page);
|
|
46
|
+
if (page.length < limit)
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
return rows;
|
|
50
|
+
}
|
|
32
51
|
/**
|
|
33
52
|
* The public list endpoints are NRN-authorized and need explicit scoping (account needs
|
|
34
53
|
* organization_id, namespace needs account_id, application needs namespace_id). There is no
|
|
@@ -53,16 +72,17 @@ export class NpContext {
|
|
|
53
72
|
if (!refresh && this.skeleton && Date.now() - this.skeleton.at < NpContext.CACHE_TTL_MS)
|
|
54
73
|
return this.skeleton;
|
|
55
74
|
const orgId = await this.organizationId();
|
|
56
|
-
const
|
|
57
|
-
organization_id: orgId,
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const perAccount = await pmap(
|
|
61
|
-
const
|
|
62
|
-
.get("/namespace", { account_id: account.id, limit
|
|
63
|
-
.
|
|
75
|
+
const accounts = await fetchPaged((offset, limit) => this.np
|
|
76
|
+
.get("/account", { organization_id: orgId, offset, limit })
|
|
77
|
+
.then((page) => page.results ?? [])
|
|
78
|
+
.catch(() => []), SKELETON_CAP);
|
|
79
|
+
const perAccount = await pmap(accounts, async (account) => {
|
|
80
|
+
const accountNamespaces = await fetchPaged((offset, limit) => this.np
|
|
81
|
+
.get("/namespace", { account_id: account.id, offset, limit })
|
|
82
|
+
.then((page) => page.results ?? [])
|
|
83
|
+
.catch(() => []), SKELETON_CAP);
|
|
64
84
|
const accountBaseRepoUrl = baseRepoUrl(account.repository_provider, account.repository_prefix);
|
|
65
|
-
return
|
|
85
|
+
return accountNamespaces.map((namespace) => ({
|
|
66
86
|
id: namespace.id,
|
|
67
87
|
name: namespace.name,
|
|
68
88
|
nrn: namespace.nrn,
|
|
@@ -109,21 +129,12 @@ export class NpContext {
|
|
|
109
129
|
? skeleton.namespaces.filter((namespace) => namespace.name.toLowerCase().includes(namespaceFilter))
|
|
110
130
|
: skeleton.namespaces;
|
|
111
131
|
const nameFilter = args.query?.toLowerCase();
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (args.query)
|
|
119
|
-
query["name:contains"] = args.query; // server-side when supported
|
|
120
|
-
const page = await this.np.get("/application", query);
|
|
121
|
-
return (page.results ?? []).map((app) => this.mapApp(app, namespace));
|
|
122
|
-
}
|
|
123
|
-
catch {
|
|
124
|
-
return [];
|
|
125
|
-
}
|
|
126
|
-
});
|
|
132
|
+
// `/application` is namespace-scoped with no name filter, so page each namespace to completion
|
|
133
|
+
// and filter by name client-side (below). This lifts the old silent 200-per-org cap.
|
|
134
|
+
const perNamespace = await pmap(namespaces, async (namespace) => fetchPaged((offset, limit) => this.np
|
|
135
|
+
.get("/application", { namespace_id: namespace.id, offset, limit })
|
|
136
|
+
.then((page) => page.results ?? [])
|
|
137
|
+
.catch(() => []), MAX_APPS).then((rows) => rows.map((app) => this.mapApp(app, namespace))));
|
|
127
138
|
const seen = new Set();
|
|
128
139
|
return perNamespace
|
|
129
140
|
.flat()
|
package/dist/np/journey.js
CHANGED
|
@@ -11,13 +11,24 @@ const TERMINAL = new Set([
|
|
|
11
11
|
]);
|
|
12
12
|
export const isDeploymentTerminal = (status) => !!status && TERMINAL.has(status);
|
|
13
13
|
export function bumpSemver(semver) {
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
// Preserve the optional `v` prefix: an org that enforces a v-prefixed version pattern
|
|
15
|
+
// rejects a bare bump, and dropping it makes the release history inconsistent.
|
|
16
|
+
const parts = /^(v?)(\d+)\.(\d+)\.(\d+)/.exec(semver ?? "");
|
|
17
|
+
return parts ? `${parts[1]}${parts[2]}.${parts[3]}.${Number(parts[4]) + 1}` : "0.0.1";
|
|
16
18
|
}
|
|
17
|
-
export async function listBuilds(np, applicationId,
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
export async function listBuilds(np, applicationId, options = {}) {
|
|
20
|
+
const query = {
|
|
21
|
+
application_id: applicationId,
|
|
22
|
+
sort: "created_at:desc",
|
|
23
|
+
limit: options.limit ?? 10,
|
|
24
|
+
};
|
|
25
|
+
// The public API filters builds by full commit SHA via the `commit.id` query param (in the
|
|
26
|
+
// canonical OpenAPI contract) — this is how a change is traced from a commit to its build.
|
|
27
|
+
if (options.commit)
|
|
28
|
+
query["commit.id"] = options.commit;
|
|
29
|
+
if (options.offset)
|
|
30
|
+
query.offset = options.offset;
|
|
31
|
+
const page = await np.get("/build", query).catch(() => ({ results: [] }));
|
|
21
32
|
return (page.results ?? []).map((build) => ({
|
|
22
33
|
id: build.id,
|
|
23
34
|
status: build.status,
|
|
@@ -43,6 +54,8 @@ export async function listReleases(np, applicationId, options = {}) {
|
|
|
43
54
|
};
|
|
44
55
|
if (options.status)
|
|
45
56
|
query.status = options.status;
|
|
57
|
+
if (options.offset)
|
|
58
|
+
query.offset = options.offset;
|
|
46
59
|
const page = await np
|
|
47
60
|
.get("/release", query)
|
|
48
61
|
.catch(() => ({ results: [] }));
|
|
@@ -186,6 +199,25 @@ export async function listScopeDeployments(np, scopeId, limit = 3) {
|
|
|
186
199
|
.catch(() => ({ results: [] }));
|
|
187
200
|
return (page.results ?? []).map(mapDeployment);
|
|
188
201
|
}
|
|
202
|
+
/** Every deployment of an application, newest first — the public `/deployment` endpoint accepts
|
|
203
|
+
* `application_id`. Group rows (`type: "deployment_group"`) carry their per-scope children. */
|
|
204
|
+
export async function listDeployments(np, applicationId, options = {}) {
|
|
205
|
+
const query = {
|
|
206
|
+
application_id: applicationId,
|
|
207
|
+
sort: "created_at:desc",
|
|
208
|
+
limit: options.limit ?? 50,
|
|
209
|
+
};
|
|
210
|
+
if (options.offset)
|
|
211
|
+
query.offset = options.offset;
|
|
212
|
+
const page = await np
|
|
213
|
+
.get("/deployment", query)
|
|
214
|
+
.catch(() => ({ results: [] }));
|
|
215
|
+
return (page.results ?? []).map((row) => ({
|
|
216
|
+
...mapDeployment(row),
|
|
217
|
+
type: row.type,
|
|
218
|
+
deployments: row.deployments?.map(mapDeployment),
|
|
219
|
+
}));
|
|
220
|
+
}
|
|
189
221
|
/**
|
|
190
222
|
* Traffic switch — the public API takes desiredSwitchedTraffic (camelCase INSIDE the
|
|
191
223
|
* snake_case strategy_data envelope; verified live) and moves traffic toward it.
|
|
@@ -234,9 +266,12 @@ export async function setParameters(np, nrn, params) {
|
|
|
234
266
|
return { created, updated };
|
|
235
267
|
}
|
|
236
268
|
/** Read parameters — NRN-scoped on the public API; returns undefined when unavailable. */
|
|
237
|
-
export async function listParameters(np, nrn) {
|
|
269
|
+
export async function listParameters(np, nrn, options = {}) {
|
|
238
270
|
try {
|
|
239
|
-
const
|
|
271
|
+
const query = { nrn, limit: options.limit ?? 100 };
|
|
272
|
+
if (options.offset)
|
|
273
|
+
query.offset = options.offset;
|
|
274
|
+
const page = await np.get("/parameter", query);
|
|
240
275
|
return (page.results ?? []).map((parameter) => ({
|
|
241
276
|
id: parameter.id,
|
|
242
277
|
name: parameter.name,
|
|
@@ -6,7 +6,9 @@ import { tools } from "../tools/index.js";
|
|
|
6
6
|
*/
|
|
7
7
|
const INSTRUCTIONS = `nullplatform is where this code gets built, released, deployed and observed — these tools replace its web dashboard for the everyday developer journey.
|
|
8
8
|
|
|
9
|
-
The tools are repo-aware: inside a git repo, omit \`app\` and the linked application is inferred from the git remote. Start with \`application_get\` — it shows what's live where and suggests the next action.
|
|
9
|
+
The tools are repo-aware: inside a git repo, omit \`app\` and the linked application is inferred from the git remote. "This app", or a request that names nothing, means the repo's app — infer it; pass \`app\` only when the user means a different, explicitly named application. Start with \`application_get\` — it shows what's live where and suggests the next action.
|
|
10
|
+
|
|
11
|
+
You run in the developer's own environment, so fuse the local repo with platform state. Read the git remote, branch, HEAD commit, the diff being shipped, and config files (\`.env\`, \`package.json\`, Dockerfile), and correlate them with what the platform reports: does the local HEAD match a built or released commit, does a local config value match what a scope resolves. That correlation is this integration's edge over the web dashboard, which only sees the platform half.
|
|
10
12
|
|
|
11
13
|
Every tool accepts \`language\`: ALWAYS set it to the language the user is conversing in (ISO code, e.g. "es", "en") — answers come back in the user's language.
|
|
12
14
|
|
package/dist/tool-names.js
CHANGED
|
@@ -12,6 +12,8 @@ export const TOOL = {
|
|
|
12
12
|
applicationLogList: "application_log_list",
|
|
13
13
|
applicationMetricList: "application_metric_list",
|
|
14
14
|
applicationBuildList: "application_build_list",
|
|
15
|
+
applicationReleaseList: "application_release_list",
|
|
16
|
+
applicationDeploymentList: "application_deployment_list",
|
|
15
17
|
applicationParameterList: "application_parameter_list",
|
|
16
18
|
applicationParameterCreate: "application_parameter_create",
|
|
17
19
|
applicationDeploymentCreate: "application_deployment_create",
|
package/dist/tools/builds.js
CHANGED
|
@@ -4,7 +4,7 @@ import { ago, glyph, next, shortCommit, table } from "../md.js";
|
|
|
4
4
|
import { listAssets, listBuilds, listReleases } from "../np/journey.js";
|
|
5
5
|
import { defineTool, reply } from "../tool.js";
|
|
6
6
|
import { TOOL } from "../tool-names.js";
|
|
7
|
-
import { appArg, requireApp } from "./shared.js";
|
|
7
|
+
import { appArg, offsetArg, pageOf, requireApp } from "./shared.js";
|
|
8
8
|
/**
|
|
9
9
|
* Closes the hole in the ship loop: between "push a commit" and "deploy" sits CI, and the
|
|
10
10
|
* agent needs to see builds land. status only shows the latest; this lists recent builds,
|
|
@@ -12,14 +12,20 @@ import { appArg, requireApp } from "./shared.js";
|
|
|
12
12
|
*/
|
|
13
13
|
export const buildsTool = defineTool({
|
|
14
14
|
name: TOOL.applicationBuildList,
|
|
15
|
-
title: "
|
|
16
|
-
description: "List an application's recent CI builds — 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.",
|
|
15
|
+
title: "Builds",
|
|
16
|
+
description: "List an application's recent CI builds — 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 (tracing a change to its build and release).",
|
|
17
17
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
18
|
+
widget: "builds",
|
|
18
19
|
errorKey: "builds.errorLabel",
|
|
19
20
|
inputSchema: {
|
|
20
21
|
app: appArg,
|
|
21
22
|
build: z.number().optional().describe("Build id — show this build's assets instead of the list"),
|
|
22
23
|
limit: z.number().optional().describe("How many recent builds (default 10)"),
|
|
24
|
+
commit: z
|
|
25
|
+
.string()
|
|
26
|
+
.optional()
|
|
27
|
+
.describe("Full commit SHA — list only the build(s) for that commit (tracing a change to its build/release)"),
|
|
28
|
+
offset: offsetArg,
|
|
23
29
|
},
|
|
24
30
|
async handler(args, context) {
|
|
25
31
|
const resolved = await requireApp(context, args);
|
|
@@ -43,11 +49,14 @@ export const buildsTool = defineTool({
|
|
|
43
49
|
return reply(markdown, { build_id: args.build, assets });
|
|
44
50
|
}
|
|
45
51
|
const [builds, releases] = await Promise.all([
|
|
46
|
-
listBuilds(context.np, app.id, args.limit ?? 10),
|
|
52
|
+
listBuilds(context.np, app.id, { limit: args.limit ?? 10, commit: args.commit, offset: args.offset }),
|
|
47
53
|
listReleases(context.np, app.id, { limit: 50 }),
|
|
48
54
|
]);
|
|
49
55
|
if (builds.length === 0) {
|
|
50
|
-
|
|
56
|
+
const empty = args.commit
|
|
57
|
+
? translate("builds.noneForCommit", { app: app.name, commit: shortCommit(args.commit) })
|
|
58
|
+
: translate("builds.none", { app: app.name }) + next(translate("builds.noneHint"));
|
|
59
|
+
return reply(empty, { app: `#${app.id}`, app_name: app.name, builds: [], commit: args.commit ?? null });
|
|
51
60
|
}
|
|
52
61
|
const releasedBuildIds = new Set(releases.map((release) => release.build_id).filter(Boolean));
|
|
53
62
|
const releaseByBuild = new Map(releases.map((release) => [release.build_id, release]));
|
|
@@ -79,6 +88,7 @@ export const buildsTool = defineTool({
|
|
|
79
88
|
released: releasedBuildIds.has(build.id),
|
|
80
89
|
release_semver: releaseByBuild.get(build.id)?.semver ?? null,
|
|
81
90
|
})),
|
|
91
|
+
page: pageOf(builds.length, args.limit ?? 10, args.offset ?? 0),
|
|
82
92
|
});
|
|
83
93
|
},
|
|
84
94
|
});
|
|
@@ -24,7 +24,7 @@ export const createReleaseTool = defineTool({
|
|
|
24
24
|
let buildId = args.build_id;
|
|
25
25
|
let buildNote = "";
|
|
26
26
|
if (!buildId) {
|
|
27
|
-
const builds = await listBuilds(context.np, app.id, 5);
|
|
27
|
+
const builds = await listBuilds(context.np, app.id, { limit: 5 });
|
|
28
28
|
const successful = builds.find((build) => build.status === "successful");
|
|
29
29
|
if (!successful)
|
|
30
30
|
return fail(translate("createRelease.noBuilds", { app: app.name }));
|
package/dist/tools/deploy.js
CHANGED
|
@@ -15,7 +15,7 @@ async function resolveRelease(context, applicationId, args) {
|
|
|
15
15
|
// code under its name — so search deep enough that only pathologically active apps miss.
|
|
16
16
|
const [releases, builds] = await Promise.all([
|
|
17
17
|
listReleases(context.np, applicationId, { status: "active", limit: 200 }),
|
|
18
|
-
listBuilds(context.np, applicationId, 5),
|
|
18
|
+
listBuilds(context.np, applicationId, { limit: 5 }),
|
|
19
19
|
]);
|
|
20
20
|
const successfulBuild = builds.find((build) => build.status === "successful");
|
|
21
21
|
if (args.release_id) {
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { translate } from "../i18n.js";
|
|
3
|
+
import { ago, glyph, next, table } from "../md.js";
|
|
4
|
+
import { listDeployments, listReleases, listScopes } from "../np/journey.js";
|
|
5
|
+
import { defineTool, reply } from "../tool.js";
|
|
6
|
+
import { TOOL } from "../tool-names.js";
|
|
7
|
+
import { appArg, offsetArg, pageOf, requireApp } from "./shared.js";
|
|
8
|
+
/**
|
|
9
|
+
* The deployments list — the end of build → release → deploy. Lists every deployment across an
|
|
10
|
+
* app's scopes (a deployment_group lands one release on several scopes and is shown as one row),
|
|
11
|
+
* resolving scope and release names so both the text reply and the bound widget read cleanly.
|
|
12
|
+
*/
|
|
13
|
+
export const deploymentsTool = defineTool({
|
|
14
|
+
name: TOOL.applicationDeploymentList,
|
|
15
|
+
title: "Deployments",
|
|
16
|
+
description: "List an application's deployments across all scopes — which release landed on which scope, the rollout status, and age; a deployment group (one release to several scopes) is shown as one row. Use to see deploy history or find an active rollout.",
|
|
17
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
18
|
+
widget: "deployments",
|
|
19
|
+
errorKey: "deploymentList.errorLabel",
|
|
20
|
+
inputSchema: {
|
|
21
|
+
app: appArg,
|
|
22
|
+
limit: z.number().optional().describe("How many recent deployments (default 50)"),
|
|
23
|
+
offset: offsetArg,
|
|
24
|
+
},
|
|
25
|
+
async handler(args, context) {
|
|
26
|
+
const resolved = await requireApp(context, args);
|
|
27
|
+
if ("out" in resolved)
|
|
28
|
+
return resolved.out;
|
|
29
|
+
const app = resolved.app;
|
|
30
|
+
const [rows, scopes, releases] = await Promise.all([
|
|
31
|
+
listDeployments(context.np, app.id, { limit: args.limit ?? 50, offset: args.offset }),
|
|
32
|
+
listScopes(context.np, app.id),
|
|
33
|
+
listReleases(context.np, app.id, { limit: 200 }),
|
|
34
|
+
]);
|
|
35
|
+
if (rows.length === 0) {
|
|
36
|
+
return reply(translate("deploymentList.none", { app: app.name }), {
|
|
37
|
+
app: `#${app.id}`,
|
|
38
|
+
app_name: app.name,
|
|
39
|
+
deployments: [],
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
const scopeName = new Map(scopes.map((scope) => [scope.id, scope.name]));
|
|
43
|
+
const semverOf = new Map(releases.map((release) => [release.id, release.semver]));
|
|
44
|
+
const scopeLabel = (row) => row.type === "deployment_group"
|
|
45
|
+
? translate("deploymentList.group", { count: row.deployments?.length ?? 0 })
|
|
46
|
+
: (scopeName.get(row.scope_id ?? -1) ?? `scope #${row.scope_id ?? "?"}`);
|
|
47
|
+
const releaseLabel = (releaseId) => releaseId ? (semverOf.get(releaseId) ?? `#${releaseId}`) : "";
|
|
48
|
+
const markdown = [
|
|
49
|
+
translate("deploymentList.title", { app: app.name, count: rows.length }),
|
|
50
|
+
"",
|
|
51
|
+
table([
|
|
52
|
+
translate("header.scope"),
|
|
53
|
+
translate("header.release"),
|
|
54
|
+
translate("header.status"),
|
|
55
|
+
translate("header.when"),
|
|
56
|
+
], rows.map((row) => [
|
|
57
|
+
scopeLabel(row),
|
|
58
|
+
releaseLabel(row.release_id),
|
|
59
|
+
`${glyph(row.status)} ${row.status}`,
|
|
60
|
+
ago(row.created_at),
|
|
61
|
+
])),
|
|
62
|
+
next(translate("deploymentList.hint")),
|
|
63
|
+
].join("\n");
|
|
64
|
+
// Enrich the structured rows the widget renders: scope + release names resolved, children too.
|
|
65
|
+
const enrich = (row) => ({
|
|
66
|
+
...row,
|
|
67
|
+
scope_name: scopeName.get(row.scope_id ?? -1) ?? null,
|
|
68
|
+
release_semver: row.release_id ? (semverOf.get(row.release_id) ?? null) : null,
|
|
69
|
+
});
|
|
70
|
+
const deployments = rows.map((row) => ({
|
|
71
|
+
...enrich(row),
|
|
72
|
+
deployments: row.deployments?.map(enrich),
|
|
73
|
+
}));
|
|
74
|
+
return reply(markdown, {
|
|
75
|
+
app: `#${app.id}`,
|
|
76
|
+
app_name: app.name,
|
|
77
|
+
deployments,
|
|
78
|
+
page: pageOf(rows.length, args.limit ?? 50, args.offset ?? 0),
|
|
79
|
+
});
|
|
80
|
+
},
|
|
81
|
+
});
|
package/dist/tools/find-apps.js
CHANGED
|
@@ -7,7 +7,7 @@ import { defineTool, reply } from "../tool.js";
|
|
|
7
7
|
import { TOOL } from "../tool-names.js";
|
|
8
8
|
export const findAppsTool = defineTool({
|
|
9
9
|
name: TOOL.applicationList,
|
|
10
|
-
title: "
|
|
10
|
+
title: "Applications",
|
|
11
11
|
description: "Search applications across the whole organization by partial name (and optionally namespace). Fast (parallel + cached). Use when the repo isn't linked or the user names an app you don't know.",
|
|
12
12
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
13
13
|
widget: "find-apps",
|
package/dist/tools/index.js
CHANGED
|
@@ -4,12 +4,14 @@ import { createAppTool } from "./create-app.js";
|
|
|
4
4
|
import { createReleaseTool } from "./create-release.js";
|
|
5
5
|
import { createScopeTool } from "./create-scope.js";
|
|
6
6
|
import { deployTool } from "./deploy.js";
|
|
7
|
+
import { deploymentsTool } from "./deployments.js";
|
|
7
8
|
import { findAppsTool } from "./find-apps.js";
|
|
8
9
|
import { logsTool } from "./logs.js";
|
|
9
10
|
import { metricsTool } from "./metrics.js";
|
|
10
11
|
import { overviewTool } from "./overview.js";
|
|
11
12
|
import { paramsTool } from "./params.js";
|
|
12
13
|
import { playbookGetTool } from "./playbook.js";
|
|
14
|
+
import { releasesTool } from "./releases.js";
|
|
13
15
|
import { servicesTool } from "./services.js";
|
|
14
16
|
import { setParamsTool } from "./set-params.js";
|
|
15
17
|
import { statusTool } from "./status.js";
|
|
@@ -24,6 +26,8 @@ export const tools = [
|
|
|
24
26
|
overviewTool,
|
|
25
27
|
findAppsTool,
|
|
26
28
|
buildsTool,
|
|
29
|
+
releasesTool,
|
|
30
|
+
deploymentsTool,
|
|
27
31
|
logsTool,
|
|
28
32
|
paramsTool,
|
|
29
33
|
metricsTool,
|
package/dist/tools/logs.js
CHANGED
|
@@ -7,7 +7,7 @@ import { TOOL } from "../tool-names.js";
|
|
|
7
7
|
import { appArg, chooseScope, requireApp } from "./shared.js";
|
|
8
8
|
export const logsTool = defineTool({
|
|
9
9
|
name: TOOL.applicationLogList,
|
|
10
|
-
title: "
|
|
10
|
+
title: "Logs",
|
|
11
11
|
description: "Read recent application logs (optionally for one scope). Returns the latest lines, newest last — good for a quick 'why is it failing?' look.",
|
|
12
12
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
13
13
|
widget: "logs",
|
package/dist/tools/params.js
CHANGED
|
@@ -3,15 +3,15 @@ import { dashboardLink, linkLine, next, table } from "../md.js";
|
|
|
3
3
|
import { listParameters } from "../np/journey.js";
|
|
4
4
|
import { defineTool, fail, reply } from "../tool.js";
|
|
5
5
|
import { TOOL } from "../tool-names.js";
|
|
6
|
-
import { appArg, requireApp } from "./shared.js";
|
|
6
|
+
import { appArg, offsetArg, pageOf, requireApp } from "./shared.js";
|
|
7
7
|
export const paramsTool = defineTool({
|
|
8
8
|
name: TOOL.applicationParameterList,
|
|
9
|
-
title: "
|
|
9
|
+
title: "Parameters",
|
|
10
10
|
description: "List an application's configuration parameters (env vars / files; secret values are masked). Use application_parameter_create to add or change them.",
|
|
11
11
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
12
12
|
widget: "params",
|
|
13
13
|
errorKey: "params.errorLabel",
|
|
14
|
-
inputSchema: { app: appArg },
|
|
14
|
+
inputSchema: { app: appArg, offset: offsetArg },
|
|
15
15
|
async handler(args, context) {
|
|
16
16
|
const resolved = await requireApp(context, args);
|
|
17
17
|
if ("out" in resolved)
|
|
@@ -20,7 +20,7 @@ export const paramsTool = defineTool({
|
|
|
20
20
|
if (!app.nrn)
|
|
21
21
|
return fail(translate("resolve.noNrn", { app: app.name }));
|
|
22
22
|
const [parameters, orgSlug] = await Promise.all([
|
|
23
|
-
listParameters(context.np, app.nrn),
|
|
23
|
+
listParameters(context.np, app.nrn, { offset: args.offset }),
|
|
24
24
|
context.org.organizationSlug(),
|
|
25
25
|
]);
|
|
26
26
|
const dashboard = dashboardLink(orgSlug, app.nrn);
|
|
@@ -53,6 +53,11 @@ export const paramsTool = defineTool({
|
|
|
53
53
|
])),
|
|
54
54
|
next(translate("params.applyHint")),
|
|
55
55
|
].join("\n");
|
|
56
|
-
return reply(markdown, {
|
|
56
|
+
return reply(markdown, {
|
|
57
|
+
available: true,
|
|
58
|
+
params: parameters,
|
|
59
|
+
page: pageOf(parameters.length, 100, args.offset ?? 0),
|
|
60
|
+
...appRef,
|
|
61
|
+
});
|
|
57
62
|
},
|
|
58
63
|
});
|
package/dist/tools/playbook.js
CHANGED
|
@@ -13,8 +13,8 @@ import { TOOL } from "../tool-names.js";
|
|
|
13
13
|
const playbookBody = (markdown) => markdown.replace(/^---\n[\s\S]*?\n---\n?/, "").trim();
|
|
14
14
|
export const playbookGetTool = defineTool({
|
|
15
15
|
name: TOOL.playbookGet,
|
|
16
|
-
title: "
|
|
17
|
-
description: "Read a nullplatform operating playbook — the methodology to follow
|
|
16
|
+
title: "Operating playbooks",
|
|
17
|
+
description: "Read a nullplatform operating playbook — the methodology to follow BEFORE the matching non-trivial work (e.g. deploying-safely before a deploy, incident-response when something is broken, configuring-safely before changing parameters or secrets). The server instructions carry the full catalog; call this with no name to list them too.",
|
|
18
18
|
annotations: { readOnlyHint: true },
|
|
19
19
|
errorKey: "playbook.errorLabel",
|
|
20
20
|
inputSchema: {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { translate } from "../i18n.js";
|
|
3
|
+
import { ago, glyph, next, table } from "../md.js";
|
|
4
|
+
import { listReleases } from "../np/journey.js";
|
|
5
|
+
import { defineTool, reply } from "../tool.js";
|
|
6
|
+
import { TOOL } from "../tool-names.js";
|
|
7
|
+
import { appArg, offsetArg, pageOf, requireApp } from "./shared.js";
|
|
8
|
+
/**
|
|
9
|
+
* The releases list — the middle of build → release → deploy. A release is an immutable semver
|
|
10
|
+
* pointer to a build; this lists them so the model (and the bound widget) can pick one to deploy.
|
|
11
|
+
*/
|
|
12
|
+
export const releasesTool = defineTool({
|
|
13
|
+
name: TOOL.applicationReleaseList,
|
|
14
|
+
title: "Releases",
|
|
15
|
+
description: "List an application's releases — 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.",
|
|
16
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
17
|
+
widget: "releases",
|
|
18
|
+
errorKey: "releaseList.errorLabel",
|
|
19
|
+
inputSchema: {
|
|
20
|
+
app: appArg,
|
|
21
|
+
limit: z.number().optional().describe("How many recent releases (default 25)"),
|
|
22
|
+
offset: offsetArg,
|
|
23
|
+
},
|
|
24
|
+
async handler(args, context) {
|
|
25
|
+
const resolved = await requireApp(context, args);
|
|
26
|
+
if ("out" in resolved)
|
|
27
|
+
return resolved.out;
|
|
28
|
+
const app = resolved.app;
|
|
29
|
+
const releases = await listReleases(context.np, app.id, {
|
|
30
|
+
limit: args.limit ?? 25,
|
|
31
|
+
offset: args.offset,
|
|
32
|
+
});
|
|
33
|
+
if (releases.length === 0) {
|
|
34
|
+
return reply(translate("releaseList.none", { app: app.name }) + next(translate("releaseList.noneHint")), { app: `#${app.id}`, app_name: app.name, releases: [] });
|
|
35
|
+
}
|
|
36
|
+
const markdown = [
|
|
37
|
+
translate("releaseList.title", { app: app.name, count: releases.length }),
|
|
38
|
+
"",
|
|
39
|
+
table([
|
|
40
|
+
translate("header.version"),
|
|
41
|
+
translate("header.status"),
|
|
42
|
+
translate("header.build"),
|
|
43
|
+
translate("header.when"),
|
|
44
|
+
], releases.map((release) => [
|
|
45
|
+
release.semver,
|
|
46
|
+
`${glyph(release.status)} ${release.status}`,
|
|
47
|
+
release.build_id ? `#${release.build_id}` : "",
|
|
48
|
+
ago(release.created_at),
|
|
49
|
+
])),
|
|
50
|
+
next(translate("releaseList.deployHint")),
|
|
51
|
+
].join("\n");
|
|
52
|
+
return reply(markdown, {
|
|
53
|
+
app: `#${app.id}`,
|
|
54
|
+
app_name: app.name,
|
|
55
|
+
releases,
|
|
56
|
+
page: pageOf(releases.length, args.limit ?? 25, args.offset ?? 0),
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
});
|
package/dist/tools/shared.js
CHANGED
|
@@ -11,6 +11,19 @@ export const appArg = z
|
|
|
11
11
|
.string()
|
|
12
12
|
.optional()
|
|
13
13
|
.describe('Application name or "#id". Omit it to use the app linked to the current repo (git remote).');
|
|
14
|
+
export const offsetArg = z
|
|
15
|
+
.number()
|
|
16
|
+
.optional()
|
|
17
|
+
.describe("Skip this many items to page through a long list (use page.next_offset from a prior call).");
|
|
18
|
+
/**
|
|
19
|
+
* Pagination token for a list reply. No list endpoint returns a total, so the boundary is the
|
|
20
|
+
* standard offset heuristic: a full page means there is likely more. The widget/model pages by
|
|
21
|
+
* re-calling the tool with `offset: next_offset` until `has_more` is false.
|
|
22
|
+
*/
|
|
23
|
+
export function pageOf(count, limit, offset = 0) {
|
|
24
|
+
const hasMore = count >= limit;
|
|
25
|
+
return { has_more: hasMore, next_offset: hasMore ? offset + limit : null };
|
|
26
|
+
}
|
|
14
27
|
export function httpsRepoUrl(url) {
|
|
15
28
|
const trimmed = url.trim().replace(/\.git$/, "");
|
|
16
29
|
const sshForm = /^git@([^:]+):(.+)$/.exec(trimmed);
|
package/dist/tools/status.js
CHANGED
|
@@ -26,7 +26,7 @@ async function scopeViews(context, applicationId) {
|
|
|
26
26
|
}
|
|
27
27
|
export const statusTool = defineTool({
|
|
28
28
|
name: TOOL.applicationGet,
|
|
29
|
-
title: "
|
|
29
|
+
title: "Application status",
|
|
30
30
|
description: "THE place to start. Shows an application's full picture: scopes with what's live on each (release + traffic), latest build, latest release, and the one obvious next action. Call with no arguments inside a repo to use the linked app. Pass deployment:<id> to watch one rollout in detail.",
|
|
31
31
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
32
32
|
widget: "np-panel",
|
|
@@ -54,7 +54,7 @@ export const statusTool = defineTool({
|
|
|
54
54
|
return resolved.out;
|
|
55
55
|
const [views, builds, releases, orgSlug] = await Promise.all([
|
|
56
56
|
scopeViews(context, resolved.app.id),
|
|
57
|
-
listBuilds(context.np, resolved.app.id, 5),
|
|
57
|
+
listBuilds(context.np, resolved.app.id, { limit: 5 }),
|
|
58
58
|
listReleases(context.np, resolved.app.id, { limit: 5 }),
|
|
59
59
|
context.org.organizationSlug(),
|
|
60
60
|
]);
|
package/dist/tools/traffic.js
CHANGED
|
@@ -8,8 +8,8 @@ import { TOOL } from "../tool-names.js";
|
|
|
8
8
|
import { appArg, delays, requireApp, sleep } from "./shared.js";
|
|
9
9
|
export const trafficTool = defineTool({
|
|
10
10
|
name: TOOL.applicationDeploymentUpdate,
|
|
11
|
-
title: "
|
|
12
|
-
description: 'Drive an in-flight rollout: move traffic to the new version (percent snaps to 1,5,10,25,50,75,90,95,99,100), finalize it (action:"finalize" — retire the old version), or roll it back (action:"rollback" — traffic returns to the old version). Finds the app\'s active deployment automatically.',
|
|
11
|
+
title: "Update traffic",
|
|
12
|
+
description: 'Drive an in-flight rollout: move traffic to the new version (percent snaps to 0,1,5,10,25,50,75,90,95,99,100), finalize it (action:"finalize" — retire the old version), or roll it back (action:"rollback" — traffic returns to the old version). Finds the app\'s active deployment automatically.',
|
|
13
13
|
annotations: { destructiveHint: true, openWorldHint: true },
|
|
14
14
|
widget: "np-panel",
|
|
15
15
|
errorKey: "traffic.errorLabel",
|