@nullplatform/mcp 0.1.3 → 0.1.5
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 +26 -0
- package/dist/np/journey.js +28 -2
- package/dist/tool-names.js +2 -0
- package/dist/tools/approvals.js +1 -0
- package/dist/tools/builds.js +5 -2
- package/dist/tools/create-release.js +10 -4
- package/dist/tools/create-scope.js +20 -6
- package/dist/tools/deployments.js +95 -0
- package/dist/tools/index.js +4 -0
- package/dist/tools/logs.js +5 -0
- package/dist/tools/overview.js +3 -0
- package/dist/tools/params.js +9 -4
- package/dist/tools/playbook.js +1 -0
- package/dist/tools/releases.js +59 -0
- package/dist/tools/services.js +4 -1
- package/dist/tools/set-params.js +9 -2
- package/dist/tools/shared.js +13 -0
- package/dist/tools/status.js +15 -13
- package/dist/ui.js +7 -0
- package/package.json +2 -1
- package/widgets-dist/approvals.html +910 -0
- package/widgets-dist/builds.html +910 -0
- package/widgets-dist/create-app.html +100 -19
- package/widgets-dist/deployments.html +912 -0
- package/widgets-dist/find-apps.html +108 -27
- package/widgets-dist/logs.html +102 -21
- package/widgets-dist/manifest.json +13 -6
- package/widgets-dist/metrics.html +101 -20
- package/widgets-dist/np-panel.html +103 -22
- package/widgets-dist/overview.html +912 -0
- package/widgets-dist/params.html +101 -20
- package/widgets-dist/playbook.html +914 -0
- package/widgets-dist/releases.html +912 -0
- package/widgets-dist/services.html +910 -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_",
|
|
@@ -168,6 +171,16 @@ const english = {
|
|
|
168
171
|
"builds.assetsTitle": "Assets of build #{build}",
|
|
169
172
|
"builds.noAssets": "Build #{build} has no assets (it may still be building).",
|
|
170
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",
|
|
171
184
|
// — organization_get tool —
|
|
172
185
|
"overview.title": "Organization overview · {count} application(s) scanned",
|
|
173
186
|
"overview.truncated": '_(showing the first {count} — narrow with `organization_get query:"..."`)_',
|
|
@@ -325,6 +338,9 @@ const spanish = {
|
|
|
325
338
|
"header.live": "En vivo",
|
|
326
339
|
"header.traffic": "Tráfico",
|
|
327
340
|
"header.when": "Cuándo",
|
|
341
|
+
"header.version": "Versión",
|
|
342
|
+
"header.build": "Build",
|
|
343
|
+
"header.release": "Release",
|
|
328
344
|
"md.next": "Siguiente",
|
|
329
345
|
"md.none": "_ninguno_",
|
|
330
346
|
"md.justNow": "recién",
|
|
@@ -432,6 +448,16 @@ const spanish = {
|
|
|
432
448
|
"builds.assetsTitle": "Assets del build #{build}",
|
|
433
449
|
"builds.noAssets": "El build #{build} no tiene assets (puede estar compilando todavía).",
|
|
434
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",
|
|
435
461
|
// — organization_get tool —
|
|
436
462
|
"overview.title": "Panorama de la organización · {count} aplicación(es) escaneada(s)",
|
|
437
463
|
"overview.truncated": '_(muestro las primeras {count} — acotá con `organization_get query:"..."`)_',
|
package/dist/np/journey.js
CHANGED
|
@@ -26,6 +26,8 @@ export async function listBuilds(np, applicationId, options = {}) {
|
|
|
26
26
|
// canonical OpenAPI contract) — this is how a change is traced from a commit to its build.
|
|
27
27
|
if (options.commit)
|
|
28
28
|
query["commit.id"] = options.commit;
|
|
29
|
+
if (options.offset)
|
|
30
|
+
query.offset = options.offset;
|
|
29
31
|
const page = await np.get("/build", query).catch(() => ({ results: [] }));
|
|
30
32
|
return (page.results ?? []).map((build) => ({
|
|
31
33
|
id: build.id,
|
|
@@ -52,6 +54,8 @@ export async function listReleases(np, applicationId, options = {}) {
|
|
|
52
54
|
};
|
|
53
55
|
if (options.status)
|
|
54
56
|
query.status = options.status;
|
|
57
|
+
if (options.offset)
|
|
58
|
+
query.offset = options.offset;
|
|
55
59
|
const page = await np
|
|
56
60
|
.get("/release", query)
|
|
57
61
|
.catch(() => ({ results: [] }));
|
|
@@ -195,6 +199,25 @@ export async function listScopeDeployments(np, scopeId, limit = 3) {
|
|
|
195
199
|
.catch(() => ({ results: [] }));
|
|
196
200
|
return (page.results ?? []).map(mapDeployment);
|
|
197
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
|
+
}
|
|
198
221
|
/**
|
|
199
222
|
* Traffic switch — the public API takes desiredSwitchedTraffic (camelCase INSIDE the
|
|
200
223
|
* snake_case strategy_data envelope; verified live) and moves traffic toward it.
|
|
@@ -243,9 +266,12 @@ export async function setParameters(np, nrn, params) {
|
|
|
243
266
|
return { created, updated };
|
|
244
267
|
}
|
|
245
268
|
/** Read parameters — NRN-scoped on the public API; returns undefined when unavailable. */
|
|
246
|
-
export async function listParameters(np, nrn) {
|
|
269
|
+
export async function listParameters(np, nrn, options = {}) {
|
|
247
270
|
try {
|
|
248
|
-
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);
|
|
249
275
|
return (page.results ?? []).map((parameter) => ({
|
|
250
276
|
id: parameter.id,
|
|
251
277
|
name: parameter.name,
|
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/approvals.js
CHANGED
|
@@ -16,6 +16,7 @@ export const approvalsTool = defineTool({
|
|
|
16
16
|
title: "Approvals",
|
|
17
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. Both use your own permissions — the platform denies what you\'re not allowed to do. Use this when a deploy is stuck in creating_approval.',
|
|
18
18
|
annotations: { destructiveHint: false, openWorldHint: true },
|
|
19
|
+
widget: "approvals",
|
|
19
20
|
errorKey: "approvals.errorLabel",
|
|
20
21
|
inputSchema: {
|
|
21
22
|
app: appArg,
|
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,
|
|
@@ -15,6 +15,7 @@ export const buildsTool = defineTool({
|
|
|
15
15
|
title: "Builds",
|
|
16
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,
|
|
@@ -24,6 +25,7 @@ export const buildsTool = defineTool({
|
|
|
24
25
|
.string()
|
|
25
26
|
.optional()
|
|
26
27
|
.describe("Full commit SHA — list only the build(s) for that commit (tracing a change to its build/release)"),
|
|
28
|
+
offset: offsetArg,
|
|
27
29
|
},
|
|
28
30
|
async handler(args, context) {
|
|
29
31
|
const resolved = await requireApp(context, args);
|
|
@@ -47,7 +49,7 @@ export const buildsTool = defineTool({
|
|
|
47
49
|
return reply(markdown, { build_id: args.build, assets });
|
|
48
50
|
}
|
|
49
51
|
const [builds, releases] = await Promise.all([
|
|
50
|
-
listBuilds(context.np, app.id, { limit: args.limit ?? 10, commit: args.commit }),
|
|
52
|
+
listBuilds(context.np, app.id, { limit: args.limit ?? 10, commit: args.commit, offset: args.offset }),
|
|
51
53
|
listReleases(context.np, app.id, { limit: 50 }),
|
|
52
54
|
]);
|
|
53
55
|
if (builds.length === 0) {
|
|
@@ -86,6 +88,7 @@ export const buildsTool = defineTool({
|
|
|
86
88
|
released: releasedBuildIds.has(build.id),
|
|
87
89
|
release_semver: releaseByBuild.get(build.id)?.semver ?? null,
|
|
88
90
|
})),
|
|
91
|
+
page: pageOf(builds.length, args.limit ?? 10, args.offset ?? 0),
|
|
89
92
|
});
|
|
90
93
|
},
|
|
91
94
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { translate } from "../i18n.js";
|
|
3
3
|
import { ago, glyph, next, shortCommit } from "../md.js";
|
|
4
|
-
import { createRelease, findActiveReleaseForBuild, listBuilds } from "../np/journey.js";
|
|
4
|
+
import { createRelease, findActiveReleaseForBuild, listBuilds, listReleases } from "../np/journey.js";
|
|
5
5
|
import { defineTool, fail, reply } from "../tool.js";
|
|
6
6
|
import { TOOL } from "../tool-names.js";
|
|
7
7
|
import { appArg, requireApp } from "./shared.js";
|
|
@@ -9,7 +9,9 @@ export const createReleaseTool = defineTool({
|
|
|
9
9
|
name: TOOL.applicationReleaseCreate,
|
|
10
10
|
title: "Create release",
|
|
11
11
|
description: "Cut an active release from a build (default: the latest successful build; semver auto-bumps the patch). Usually you can just call deploy, which does this for you.",
|
|
12
|
-
|
|
12
|
+
// Convergent under retries (reuses the active release-for-build), so idempotent.
|
|
13
|
+
annotations: { destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
14
|
+
widget: "releases",
|
|
13
15
|
errorKey: "createRelease.errorLabel",
|
|
14
16
|
inputSchema: {
|
|
15
17
|
app: appArg,
|
|
@@ -40,13 +42,17 @@ export const createReleaseTool = defineTool({
|
|
|
40
42
|
// (matching the requested semver, if any), return it instead of minting a duplicate.
|
|
41
43
|
const existing = await findActiveReleaseForBuild(context.np, app.id, buildId, args.semver);
|
|
42
44
|
if (existing) {
|
|
43
|
-
|
|
45
|
+
const releases = await listReleases(context.np, app.id, { limit: 25 }).catch(() => [existing]);
|
|
46
|
+
return reply(`${glyph("active")} ${translate("createRelease.exists", { semver: existing.semver, build: buildId })}${next(translate("createRelease.deployHint", { id: existing.id }))}`, { release: existing, reused: true, app: `#${app.id}`, app_name: app.name, releases });
|
|
44
47
|
}
|
|
45
48
|
const release = await createRelease(context.np, {
|
|
46
49
|
application_id: app.id,
|
|
47
50
|
build_id: buildId,
|
|
48
51
|
semver: args.semver,
|
|
49
52
|
});
|
|
50
|
-
|
|
53
|
+
// Mirror the releases widget's shape so the SAME panel renders the new release with its
|
|
54
|
+
// Deploy action (read-after-write); the markdown stays the plain confirmation.
|
|
55
|
+
const releases = await listReleases(context.np, app.id, { limit: 25 }).catch(() => [release]);
|
|
56
|
+
return reply(`${glyph("active")} ${translate("createRelease.done", { semver: release.semver, from: buildNote })}${next(translate("createRelease.deployHint", { id: release.id }))}`, { release, reused: false, app: `#${app.id}`, app_name: app.name, releases });
|
|
51
57
|
},
|
|
52
58
|
});
|
|
@@ -5,11 +5,14 @@ import { createScope, listScopes, listScopeTypes } from "../np/journey.js";
|
|
|
5
5
|
import { defineTool, fail, reply } from "../tool.js";
|
|
6
6
|
import { TOOL } from "../tool-names.js";
|
|
7
7
|
import { appArg, dimsLabel, requireApp } from "./shared.js";
|
|
8
|
+
import { buildAppStatus } from "./status.js";
|
|
8
9
|
export const createScopeTool = defineTool({
|
|
9
10
|
name: TOOL.applicationScopeCreate,
|
|
10
11
|
title: "Create scope",
|
|
11
12
|
description: "Create a scope (a deploy target, e.g. dev/staging/main) for an application. This provisions real infrastructure asynchronously. If the org has several scope types and none is given, it lists them to pick from.",
|
|
12
|
-
|
|
13
|
+
// Convergent under retries (reuses the scope-by-name), so idempotent.
|
|
14
|
+
annotations: { destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
15
|
+
widget: "np-panel",
|
|
13
16
|
errorKey: "createScope.errorLabel",
|
|
14
17
|
inputSchema: {
|
|
15
18
|
app: appArg,
|
|
@@ -31,10 +34,13 @@ export const createScopeTool = defineTool({
|
|
|
31
34
|
const siblings = await listScopes(context.np, app.id);
|
|
32
35
|
const existing = siblings.find((scope) => scope.name.toLowerCase() === args.name.toLowerCase());
|
|
33
36
|
if (existing) {
|
|
34
|
-
const orgSlug = await
|
|
37
|
+
const [orgSlug, status] = await Promise.all([
|
|
38
|
+
context.org.organizationSlug(),
|
|
39
|
+
buildAppStatus(context, app),
|
|
40
|
+
]);
|
|
35
41
|
return reply(translate("createScope.exists", { name: existing.name, id: existing.id, status: existing.status }) +
|
|
36
42
|
next(translate("createScope.provisioningHint", { name: existing.name })) +
|
|
37
|
-
linkLine(translate("md.dashboard"), dashboardLink(orgSlug, existing.nrn)), { scope: existing, reused: true });
|
|
43
|
+
linkLine(translate("md.dashboard"), dashboardLink(orgSlug, existing.nrn)), { ...status.structured, scope: existing, reused: true });
|
|
38
44
|
}
|
|
39
45
|
let type = args.type;
|
|
40
46
|
let provider = args.provider;
|
|
@@ -46,11 +52,14 @@ export const createScopeTool = defineTool({
|
|
|
46
52
|
provider = provider ?? onlyType.provider ?? undefined;
|
|
47
53
|
}
|
|
48
54
|
else if (scopeTypes.length > 1) {
|
|
55
|
+
// np-panel's create-scope form picks a type from scope_types; the overview rides along
|
|
56
|
+
// so a fresh app (no scopes yet) renders that form instead of an empty panel.
|
|
57
|
+
const { structured } = await buildAppStatus(context, app);
|
|
49
58
|
return reply(`${translate("createScope.whichType", { name: args.name })}\n\n${table([translate("header.type"), translate("header.name"), translate("header.provider")], scopeTypes.map((scopeType) => [
|
|
50
59
|
scopeType.type ?? "",
|
|
51
60
|
scopeType.name ?? "",
|
|
52
61
|
scopeType.provider ?? "",
|
|
53
|
-
]))}${next(translate("createScope.typeHint", { name: args.name }))}`, { scope_types: scopeTypes });
|
|
62
|
+
]))}${next(translate("createScope.typeHint", { name: args.name }))}`, { ...structured, scope_types: scopeTypes });
|
|
54
63
|
}
|
|
55
64
|
}
|
|
56
65
|
if (!type)
|
|
@@ -69,7 +78,12 @@ export const createScopeTool = defineTool({
|
|
|
69
78
|
provider,
|
|
70
79
|
dimensions,
|
|
71
80
|
});
|
|
72
|
-
|
|
81
|
+
// Hand off into np-panel: the overview now includes the provisioning scope, so the panel
|
|
82
|
+
// shows it alongside a Deploy button — the literal next move (read-after-write).
|
|
83
|
+
const [orgSlug, status] = await Promise.all([
|
|
84
|
+
context.org.organizationSlug(),
|
|
85
|
+
buildAppStatus(context, app),
|
|
86
|
+
]);
|
|
73
87
|
return reply(translate("createScope.provisioning", {
|
|
74
88
|
name: scope.name,
|
|
75
89
|
id: scope.id,
|
|
@@ -77,6 +91,6 @@ export const createScopeTool = defineTool({
|
|
|
77
91
|
dimensions: dimensions ? `, ${dimsLabel(dimensions)}` : "",
|
|
78
92
|
}) +
|
|
79
93
|
next(translate("createScope.provisioningHint", { name: scope.name })) +
|
|
80
|
-
linkLine(translate("md.dashboard"), dashboardLink(orgSlug, scope.nrn)), { scope });
|
|
94
|
+
linkLine(translate("md.dashboard"), dashboardLink(orgSlug, scope.nrn)), { ...status.structured, scope, reused: false });
|
|
81
95
|
},
|
|
82
96
|
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { translate } from "../i18n.js";
|
|
3
|
+
import { ago, glyph, next, table } from "../md.js";
|
|
4
|
+
import { isDeploymentTerminal, 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
|
+
// A group has no status of its own — derive a headline from its children (same precedence as
|
|
49
|
+
// the widget's collapsed row), so the text reply never prints "· undefined".
|
|
50
|
+
const statusOf = (row) => {
|
|
51
|
+
if (row.type !== "deployment_group")
|
|
52
|
+
return row.status;
|
|
53
|
+
const children = row.deployments ?? [];
|
|
54
|
+
if (children.some((child) => !isDeploymentTerminal(child.status)))
|
|
55
|
+
return "running";
|
|
56
|
+
if (children.some((child) => child.status === "failed" || child.status === "rolled_back"))
|
|
57
|
+
return "failed";
|
|
58
|
+
if (children.length && children.every((child) => child.status === "finalized"))
|
|
59
|
+
return "finalized";
|
|
60
|
+
return children[0]?.status ?? "pending";
|
|
61
|
+
};
|
|
62
|
+
const markdown = [
|
|
63
|
+
translate("deploymentList.title", { app: app.name, count: rows.length }),
|
|
64
|
+
"",
|
|
65
|
+
table([
|
|
66
|
+
translate("header.scope"),
|
|
67
|
+
translate("header.release"),
|
|
68
|
+
translate("header.status"),
|
|
69
|
+
translate("header.when"),
|
|
70
|
+
], rows.map((row) => [
|
|
71
|
+
scopeLabel(row),
|
|
72
|
+
releaseLabel(row.release_id),
|
|
73
|
+
`${glyph(statusOf(row))} ${statusOf(row)}`,
|
|
74
|
+
ago(row.created_at),
|
|
75
|
+
])),
|
|
76
|
+
next(translate("deploymentList.hint")),
|
|
77
|
+
].join("\n");
|
|
78
|
+
// Enrich the structured rows the widget renders: scope + release names resolved, children too.
|
|
79
|
+
const enrich = (row) => ({
|
|
80
|
+
...row,
|
|
81
|
+
scope_name: scopeName.get(row.scope_id ?? -1) ?? null,
|
|
82
|
+
release_semver: row.release_id ? (semverOf.get(row.release_id) ?? null) : null,
|
|
83
|
+
});
|
|
84
|
+
const deployments = rows.map((row) => ({
|
|
85
|
+
...enrich(row),
|
|
86
|
+
deployments: row.deployments?.map(enrich),
|
|
87
|
+
}));
|
|
88
|
+
return reply(markdown, {
|
|
89
|
+
app: `#${app.id}`,
|
|
90
|
+
app_name: app.name,
|
|
91
|
+
deployments,
|
|
92
|
+
page: pageOf(rows.length, args.limit ?? 50, args.offset ?? 0),
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
});
|
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
|
@@ -22,6 +22,10 @@ export const logsTool = defineTool({
|
|
|
22
22
|
.optional()
|
|
23
23
|
.describe("ISO-8601 start of the time window, e.g. 2026-06-13T10:00:00Z"),
|
|
24
24
|
end_time: z.string().optional().describe("ISO-8601 end of the time window (defaults to now)"),
|
|
25
|
+
page_token: z
|
|
26
|
+
.string()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe("Pagination cursor (the next_page_token from a prior call) to fetch the next page of older lines."),
|
|
25
29
|
},
|
|
26
30
|
async handler(args, context) {
|
|
27
31
|
const resolved = await requireApp(context, args);
|
|
@@ -45,6 +49,7 @@ export const logsTool = defineTool({
|
|
|
45
49
|
const page = await readLogs(context.np, {
|
|
46
50
|
application_id: app.id,
|
|
47
51
|
scope_id: scope.id,
|
|
52
|
+
next_page_token: args.page_token,
|
|
48
53
|
start_time: args.start_time,
|
|
49
54
|
end_time: args.end_time,
|
|
50
55
|
});
|
package/dist/tools/overview.js
CHANGED
|
@@ -16,6 +16,7 @@ export const overviewTool = defineTool({
|
|
|
16
16
|
title: "Organization overview",
|
|
17
17
|
description: "A cross-application health digest for the whole org: what's mid-rollout right now, and which scopes' last deployment failed or rolled back. Answers 'is anything broken?' / 'what's deploying?' without naming an app. Scans up to the first ~30 applications.",
|
|
18
18
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
19
|
+
widget: "overview",
|
|
19
20
|
errorKey: "overview.errorLabel",
|
|
20
21
|
inputSchema: {
|
|
21
22
|
query: z.string().optional().describe("Limit the scan to apps whose name contains this"),
|
|
@@ -36,6 +37,7 @@ export const overviewTool = defineTool({
|
|
|
36
37
|
if (!isDeploymentTerminal(latest.status)) {
|
|
37
38
|
active.push({
|
|
38
39
|
app: app.name,
|
|
40
|
+
app_id: app.id,
|
|
39
41
|
scope: scope.name,
|
|
40
42
|
deployment_id: latest.id,
|
|
41
43
|
status: latest.status,
|
|
@@ -46,6 +48,7 @@ export const overviewTool = defineTool({
|
|
|
46
48
|
else if (latest.status === "failed" || latest.status === "rolled_back") {
|
|
47
49
|
trouble.push({
|
|
48
50
|
app: app.name,
|
|
51
|
+
app_id: app.id,
|
|
49
52
|
scope: scope.name,
|
|
50
53
|
status: latest.status,
|
|
51
54
|
when: latest.created_at,
|
package/dist/tools/params.js
CHANGED
|
@@ -3,7 +3,7 @@ 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
9
|
title: "Parameters",
|
|
@@ -11,7 +11,7 @@ export const paramsTool = defineTool({
|
|
|
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
|
@@ -16,6 +16,7 @@ export const playbookGetTool = defineTool({
|
|
|
16
16
|
title: "Operating playbooks",
|
|
17
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
|
+
widget: "playbook",
|
|
19
20
|
errorKey: "playbook.errorLabel",
|
|
20
21
|
inputSchema: {
|
|
21
22
|
name: z.string().optional().describe('Playbook name, e.g. "deploying-safely". Omit to list them.'),
|
|
@@ -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/services.js
CHANGED
|
@@ -15,6 +15,7 @@ export const servicesTool = defineTool({
|
|
|
15
15
|
title: "Services & dependencies",
|
|
16
16
|
description: "List the dependency services (databases, queues, caches…) attached to an application, plus the catalog of dependency types available to provision. Read-only: provisioning a new dependency is a guided flow, so this links you into the dashboard to create one.",
|
|
17
17
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
18
|
+
widget: "services",
|
|
18
19
|
errorKey: "services.errorLabel",
|
|
19
20
|
inputSchema: { app: appArg },
|
|
20
21
|
async handler(args, context) {
|
|
@@ -29,7 +30,8 @@ export const servicesTool = defineTool({
|
|
|
29
30
|
listServiceSpecifications(context.bff, app.nrn),
|
|
30
31
|
context.org.organizationSlug(),
|
|
31
32
|
]);
|
|
32
|
-
const
|
|
33
|
+
const dashboardUrl = dashboardLink(orgSlug, app.nrn);
|
|
34
|
+
const dashboard = linkLine(translate("md.dashboard"), dashboardUrl);
|
|
33
35
|
const sections = [];
|
|
34
36
|
if (services.length === 0) {
|
|
35
37
|
sections.push(translate("services.none", { app: app.name }));
|
|
@@ -53,6 +55,7 @@ export const servicesTool = defineTool({
|
|
|
53
55
|
app_name: app.name,
|
|
54
56
|
services,
|
|
55
57
|
catalog,
|
|
58
|
+
dashboard: dashboardUrl,
|
|
56
59
|
});
|
|
57
60
|
},
|
|
58
61
|
});
|
package/dist/tools/set-params.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { translate } from "../i18n.js";
|
|
3
3
|
import { next } from "../md.js";
|
|
4
|
-
import { setParameters } from "../np/journey.js";
|
|
4
|
+
import { listParameters, setParameters } from "../np/journey.js";
|
|
5
5
|
import { defineTool, errorMessage, fail, reply } from "../tool.js";
|
|
6
6
|
import { TOOL } from "../tool-names.js";
|
|
7
7
|
import { appArg, requireApp } from "./shared.js";
|
|
@@ -9,7 +9,8 @@ export const setParamsTool = defineTool({
|
|
|
9
9
|
name: TOOL.applicationParameterCreate,
|
|
10
10
|
title: "Set parameters",
|
|
11
11
|
description: "Create or update application configuration parameters (environment variables or files; mark secrets). Values apply on the NEXT deploy.",
|
|
12
|
-
annotations: { destructiveHint: false, openWorldHint: true },
|
|
12
|
+
annotations: { destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
13
|
+
widget: "params",
|
|
13
14
|
errorKey: "setParams.errorLabel",
|
|
14
15
|
inputSchema: {
|
|
15
16
|
app: appArg,
|
|
@@ -44,10 +45,16 @@ export const setParamsTool = defineTool({
|
|
|
44
45
|
app: app.name,
|
|
45
46
|
names,
|
|
46
47
|
});
|
|
48
|
+
// Mirror the params widget's shape so the SAME panel renders the resulting set with its
|
|
49
|
+
// add/save affordance (read-after-write); the markdown stays the plain confirmation.
|
|
50
|
+
const parameters = await listParameters(context.np, app.nrn).catch(() => undefined);
|
|
47
51
|
return reply(summary + next(translate("setParams.applyHint")), {
|
|
48
52
|
created: result.created,
|
|
49
53
|
updated: result.updated,
|
|
50
54
|
total,
|
|
55
|
+
app: `#${app.id}`,
|
|
56
|
+
app_name: app.name,
|
|
57
|
+
...(parameters === undefined ? { available: false } : { available: true, params: parameters }),
|
|
51
58
|
});
|
|
52
59
|
}
|
|
53
60
|
catch (caught) {
|
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);
|