@nullplatform/mcp 0.1.13 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/dist/http.js +16 -12
- package/dist/i18n.js +6 -0
- package/dist/log.js +53 -0
- package/dist/np/client.js +10 -1
- package/dist/np/context.js +30 -21
- package/dist/np/journey.js +14 -4
- package/dist/render.js +12 -13
- package/dist/surfaces/developer.js +21 -4
- package/dist/tool.js +64 -6
- package/dist/tools/approvals.js +1 -1
- package/dist/tools/builds.js +1 -1
- package/dist/tools/create-app.js +158 -132
- package/dist/tools/create-link.js +82 -55
- package/dist/tools/create-release.js +1 -1
- package/dist/tools/create-scope.js +35 -23
- package/dist/tools/create-service.js +39 -25
- 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 +52 -30
- package/dist/tools/deployments.js +1 -1
- package/dist/tools/entity-get.js +1 -1
- package/dist/tools/entity-list.js +14 -6
- package/dist/tools/find-apps.js +5 -2
- package/dist/tools/logs.js +114 -17
- package/dist/tools/metrics.js +1 -1
- package/dist/tools/overview.js +1 -1
- package/dist/tools/params.js +8 -2
- 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 +18 -19
- package/dist/tools/shared.js +52 -0
- package/dist/tools/status.js +4 -4
- package/dist/tools/traffic.js +38 -26
- package/dist/tools/update-link.js +1 -1
- package/dist/tools/update-service.js +1 -1
- package/dist/ui.js +1 -1
- package/package.json +5 -1
- package/widgets-dist/approvals.html +120 -10
- package/widgets-dist/builds.html +127 -17
- package/widgets-dist/create-app.html +129 -19
- package/widgets-dist/deployments.html +132 -22
- package/widgets-dist/find-apps.html +131 -21
- package/widgets-dist/logs.html +133 -23
- package/widgets-dist/manifest.json +16 -16
- package/widgets-dist/metrics.html +120 -10
- package/widgets-dist/overview.html +136 -26
- package/widgets-dist/params.html +121 -11
- package/widgets-dist/releases.html +136 -26
- package/widgets-dist/service-action.html +124 -14
- package/widgets-dist/service-create.html +123 -13
- package/widgets-dist/service-delete.html +120 -10
- package/widgets-dist/service-link.html +123 -13
- package/widgets-dist/services.html +120 -10
- package/widgets-dist/{np-panel.html → status.html} +135 -25
|
@@ -6,13 +6,41 @@ 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
8
|
import { buildAppStatus } from "./status.js";
|
|
9
|
+
/**
|
|
10
|
+
* Resolve the scope `type` (+ provider) to provision with. An explicit `type` wins; otherwise, for
|
|
11
|
+
* an app with an NRN, auto-pick the org's single scope type. Mirrors requireApp's shape: returns
|
|
12
|
+
* { type, provider } (type left undefined when there are none — the handler fails on that), or an
|
|
13
|
+
* `out` reply — the which-type form (riding the overview) when the org offers several.
|
|
14
|
+
*/
|
|
15
|
+
async function resolveScopeType(context, app, scopeName, argType, argProvider) {
|
|
16
|
+
if (argType || !app.nrn)
|
|
17
|
+
return { type: argType, provider: argProvider };
|
|
18
|
+
const scopeTypes = await listScopeTypes(context.np, app.nrn);
|
|
19
|
+
const onlyType = scopeTypes.length === 1 ? scopeTypes[0] : undefined;
|
|
20
|
+
if (onlyType) {
|
|
21
|
+
return { type: onlyType.type ?? onlyType.name, provider: argProvider ?? onlyType.provider ?? undefined };
|
|
22
|
+
}
|
|
23
|
+
if (scopeTypes.length > 1) {
|
|
24
|
+
// The create-scope form picks a type from scope_types; the overview rides along so a fresh app
|
|
25
|
+
// (no scopes yet) renders that form instead of an empty panel.
|
|
26
|
+
const { structured } = await buildAppStatus(context, app);
|
|
27
|
+
return {
|
|
28
|
+
out: reply(`${translate("createScope.whichType", { name: scopeName })}\n\n${table([translate("header.type"), translate("header.name"), translate("header.provider")], scopeTypes.map((scopeType) => [
|
|
29
|
+
scopeType.type ?? "",
|
|
30
|
+
scopeType.name ?? "",
|
|
31
|
+
scopeType.provider ?? "",
|
|
32
|
+
]))}${next(translate("createScope.typeHint", { name: scopeName }))}`, { ...structured, scope_types: scopeTypes }),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
return { type: argType, provider: argProvider };
|
|
36
|
+
}
|
|
9
37
|
export const createScopeTool = defineTool({
|
|
10
38
|
name: TOOL.applicationScopeCreate,
|
|
11
39
|
title: "Create scope",
|
|
12
|
-
description:
|
|
40
|
+
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
41
|
// Convergent under retries (reuses the scope-by-name), so idempotent.
|
|
14
42
|
annotations: { destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
15
|
-
widget: "
|
|
43
|
+
widget: "status",
|
|
16
44
|
errorKey: "createScope.errorLabel",
|
|
17
45
|
inputSchema: {
|
|
18
46
|
app: appArg,
|
|
@@ -42,26 +70,10 @@ export const createScopeTool = defineTool({
|
|
|
42
70
|
next(translate("createScope.provisioningHint", { name: existing.name })) +
|
|
43
71
|
linkLine(translate("md.dashboard"), dashboardLink(orgSlug, existing.nrn)), { ...status.structured, scope: existing, reused: true });
|
|
44
72
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const onlyType = scopeTypes.length === 1 ? scopeTypes[0] : undefined;
|
|
50
|
-
if (onlyType) {
|
|
51
|
-
type = onlyType.type ?? onlyType.name;
|
|
52
|
-
provider = provider ?? onlyType.provider ?? undefined;
|
|
53
|
-
}
|
|
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);
|
|
58
|
-
return reply(`${translate("createScope.whichType", { name: args.name })}\n\n${table([translate("header.type"), translate("header.name"), translate("header.provider")], scopeTypes.map((scopeType) => [
|
|
59
|
-
scopeType.type ?? "",
|
|
60
|
-
scopeType.name ?? "",
|
|
61
|
-
scopeType.provider ?? "",
|
|
62
|
-
]))}${next(translate("createScope.typeHint", { name: args.name }))}`, { ...structured, scope_types: scopeTypes });
|
|
63
|
-
}
|
|
64
|
-
}
|
|
73
|
+
const typeResult = await resolveScopeType(context, app, args.name, args.type, args.provider);
|
|
74
|
+
if ("out" in typeResult)
|
|
75
|
+
return typeResult.out;
|
|
76
|
+
const { type, provider } = typeResult;
|
|
65
77
|
if (!type)
|
|
66
78
|
return fail(translate("createScope.noTypes"));
|
|
67
79
|
// Orgs with runtime dimensions usually require them — borrow the shape from a sibling scope.
|
|
@@ -78,7 +90,7 @@ export const createScopeTool = defineTool({
|
|
|
78
90
|
provider,
|
|
79
91
|
dimensions,
|
|
80
92
|
});
|
|
81
|
-
// Hand off into
|
|
93
|
+
// Hand off into the status panel: the overview now includes the provisioning scope, so the panel
|
|
82
94
|
// shows it alongside a Deploy button — the literal next move (read-after-write).
|
|
83
95
|
const [orgSlug, status] = await Promise.all([
|
|
84
96
|
context.org.organizationSlug(),
|
|
@@ -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, "-")
|
|
@@ -19,6 +19,38 @@ function matchSpecs(specs, query) {
|
|
|
19
19
|
});
|
|
20
20
|
}
|
|
21
21
|
const specCategory = (spec) => [spec.category, spec.subCategory].filter(Boolean).join(" › ");
|
|
22
|
+
/**
|
|
23
|
+
* Pick which catalog dependency to provision from the `specification_id` / `type` hints. Mirrors
|
|
24
|
+
* requireApp's shape: returns the matched spec, or an `out` reply — a `fail` when the app's catalog
|
|
25
|
+
* is empty, or the pick-spec panel when the hint is missing/ambiguous (resolved via the shared
|
|
26
|
+
* resolveChoice on name + category + sub-category, never an arbitrary first entry).
|
|
27
|
+
*/
|
|
28
|
+
async function resolveServiceSpec(context, app, hints, dashboard) {
|
|
29
|
+
const specs = await listDependencySpecs(context.np, app.nrn);
|
|
30
|
+
if (specs.length === 0) {
|
|
31
|
+
return {
|
|
32
|
+
out: fail(translate("createService.noSpecs", { app: app.name }) +
|
|
33
|
+
linkLine(translate("md.dashboard"), dashboard)),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const spec = resolveChoice(specs, { id: hints.specification_id, text: hints.type }, {
|
|
37
|
+
haystack: (candidate) => `${candidate.name} ${candidate.category ?? ""} ${candidate.subCategory ?? ""}`,
|
|
38
|
+
});
|
|
39
|
+
if (spec)
|
|
40
|
+
return { spec };
|
|
41
|
+
// None / ambiguous → return the catalog (or the narrowed matches) to pick from.
|
|
42
|
+
const typed = hints.type ? matchSpecs(specs, hints.type) : [];
|
|
43
|
+
const list = typed.length > 1 ? typed : specs;
|
|
44
|
+
const heading = translate(typed.length > 1 ? "createService.matchAmbiguous" : "createService.pickSpec", {
|
|
45
|
+
app: app.name,
|
|
46
|
+
query: hints.type ?? "",
|
|
47
|
+
count: specs.length,
|
|
48
|
+
});
|
|
49
|
+
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"))}`;
|
|
50
|
+
return {
|
|
51
|
+
out: reply(md, { mode: "pick-spec", app: `#${app.id}`, app_name: app.name, specs: list, dashboard }),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
22
54
|
/**
|
|
23
55
|
* Provision a dependency service (the service record + its create action). Linking it to an app or
|
|
24
56
|
* scope is a SEPARATE operation (`application_link_create`) — two distinct platform entities with
|
|
@@ -27,7 +59,7 @@ const specCategory = (spec) => [spec.category, spec.subCategory].filter(Boolean)
|
|
|
27
59
|
export const createServiceTool = defineTool({
|
|
28
60
|
name: TOOL.applicationServiceCreate,
|
|
29
61
|
title: "Provision service",
|
|
30
|
-
description: 'Provision a dependency service (SQL database, queue, cache, …) for an application.
|
|
62
|
+
description: 'Provision a dependency service (SQL database, queue, cache, …) for an application. ALWAYS pass a SEMANTIC `type` from what the user asked for (e.g. "postgres", "redis", "queue", "cache") — you do NOT need the exact catalog spec name/id; the tool matches your word to the real catalog and pre-selects it, so the form opens with the dependency already chosen. Do NOT open the form with no `type` and then list the catalog or recommend one in text — pre-select it. Provisioning creates real cloud resources (cost-bearing) and runs asynchronously, so call WITHOUT `provision:true` first to open the form; the user confirms by submitting it. Once it is active, LINK it to a scope with application_link_create so its connection details flow in as parameters.',
|
|
31
63
|
annotations: { destructiveHint: false, openWorldHint: true },
|
|
32
64
|
widget: "service-create",
|
|
33
65
|
errorKey: "createService.errorLabel",
|
|
@@ -60,29 +92,11 @@ export const createServiceTool = defineTool({
|
|
|
60
92
|
return fail(translate("resolve.noNrn", { app: app.name }));
|
|
61
93
|
const orgSlug = await context.org.organizationSlug();
|
|
62
94
|
const dashboard = dashboardLink(orgSlug, app.nrn);
|
|
63
|
-
// The provisionable catalog
|
|
64
|
-
const
|
|
65
|
-
if (
|
|
66
|
-
return
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
// Match the requested dependency: exact id (form submit) wins; else fuzzy `type`. None or
|
|
70
|
-
// ambiguous → return the catalog to pick from.
|
|
71
|
-
const direct = args.specification_id
|
|
72
|
-
? specs.find((spec) => spec.id === args.specification_id)
|
|
73
|
-
: undefined;
|
|
74
|
-
const matches = direct ? [direct] : args.type ? matchSpecs(specs, args.type) : [];
|
|
75
|
-
if (matches.length !== 1) {
|
|
76
|
-
const list = matches.length > 1 ? matches : specs;
|
|
77
|
-
const heading = translate(matches.length > 1 ? "createService.matchAmbiguous" : "createService.pickSpec", {
|
|
78
|
-
app: app.name,
|
|
79
|
-
query: args.type ?? "",
|
|
80
|
-
count: specs.length,
|
|
81
|
-
});
|
|
82
|
-
const md = `${heading}\n\n${table([translate("header.name"), translate("createService.category"), translate("createService.provider")], list.map((spec) => [spec.name, specCategory(spec), spec.provider ?? ""]))}${next(translate("createService.pickHint"))}`;
|
|
83
|
-
return reply(md, { mode: "pick-spec", app: `#${app.id}`, app_name: app.name, specs: list, dashboard });
|
|
84
|
-
}
|
|
85
|
-
const spec = matches[0];
|
|
95
|
+
// The provisionable catalog → pre-select the spec (or surface the picker / empty-catalog fail).
|
|
96
|
+
const resolvedSpec = await resolveServiceSpec(context, { id: app.id, name: app.name, nrn: app.nrn }, { specification_id: args.specification_id, type: args.type }, dashboard);
|
|
97
|
+
if ("out" in resolvedSpec)
|
|
98
|
+
return resolvedSpec.out;
|
|
99
|
+
const spec = resolvedSpec.spec;
|
|
86
100
|
const createAction = await serviceCreateActionSpec(context.np, spec.id);
|
|
87
101
|
const name = (args.name ?? spec.name).trim();
|
|
88
102
|
// 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
|
@@ -79,12 +79,54 @@ async function resolveRelease(context, applicationId, args) {
|
|
|
79
79
|
return { release: releases[0], note: "" };
|
|
80
80
|
return { out: fail(translate("deploy.nothing") + next(translate("deploy.nothingHint"))) };
|
|
81
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Resolve which scope to deploy to. Mirrors requireApp's shape: returns the picked scope, or an
|
|
84
|
+
* `out` reply — pickScope's own ask/fail, except a brand-new app with NO scopes gets a "create a
|
|
85
|
+
* scope first" guide listing the available scope types (the literal next move).
|
|
86
|
+
*/
|
|
87
|
+
async function resolveDeployScope(context, app, scopeHint) {
|
|
88
|
+
const scopes = await listScopes(context.np, app.id);
|
|
89
|
+
const picked = pickScope(scopes, scopeHint);
|
|
90
|
+
if ("scope" in picked)
|
|
91
|
+
return { scope: picked.scope };
|
|
92
|
+
if (scopes.length === 0 && app.nrn) {
|
|
93
|
+
const scopeTypes = await listScopeTypes(context.np, app.nrn);
|
|
94
|
+
if (scopeTypes.length) {
|
|
95
|
+
const firstType = scopeTypes[0]?.type ?? scopeTypes[0]?.name ?? "";
|
|
96
|
+
const markdown = translate("deploy.noScopeTypes", {
|
|
97
|
+
types: scopeTypes.map((scopeType) => `**${scopeType.name}**`).join(", "),
|
|
98
|
+
}) + next(translate("deploy.createScopeHint", { type: firstType }));
|
|
99
|
+
return { out: fail(markdown, { scope_types: scopeTypes }) };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return { out: picked.out };
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Resolve which asset to ship. An explicit `asset` wins; otherwise auto-pick by the scope's type.
|
|
106
|
+
* Mirrors requireApp's shape: returns the asset name (undefined when the build has none to name),
|
|
107
|
+
* or an `out` reply — the pick-asset panel when a multi-asset build can't be disambiguated.
|
|
108
|
+
*/
|
|
109
|
+
async function resolveDeployAsset(context, release, scope, assetHint) {
|
|
110
|
+
if (assetHint)
|
|
111
|
+
return { assetName: assetHint };
|
|
112
|
+
const buildId = release.build_id ?? (await getRelease(context.np, release.id).catch(() => release)).build_id;
|
|
113
|
+
if (!buildId)
|
|
114
|
+
return { assetName: undefined };
|
|
115
|
+
const assets = await listAssets(context.np, buildId);
|
|
116
|
+
const choice = pickAsset(assets, scope.type);
|
|
117
|
+
if ("ambiguous" in choice) {
|
|
118
|
+
return {
|
|
119
|
+
out: reply(`${translate("deploy.multiAsset", { build: buildId, scope: scope.name, type: scope.type ?? "?" })}\n\n${table([translate("header.asset"), translate("header.type"), translate("header.platform")], choice.ambiguous.map((asset) => [asset.name, asset.type, asset.platform ?? ""]))}${next(translate("deploy.assetHint", { name: choice.ambiguous[0]?.name ?? "" }))}`, { assets: choice.ambiguous }),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return { assetName: choice.name };
|
|
123
|
+
}
|
|
82
124
|
export const deployTool = defineTool({
|
|
83
125
|
name: TOOL.applicationDeploymentCreate,
|
|
84
126
|
title: "Deploy",
|
|
85
|
-
description: '
|
|
127
|
+
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
128
|
annotations: { destructiveHint: false, openWorldHint: true },
|
|
87
|
-
widget: "
|
|
129
|
+
widget: "status",
|
|
88
130
|
errorKey: "deploy.errorLabel",
|
|
89
131
|
// The platform reports a missing/incompatible asset choice as a cross-app mismatch.
|
|
90
132
|
onError: (message) => /different applications/i.test(message) ? fail(translate("deploy.rejected", { message })) : undefined,
|
|
@@ -111,39 +153,19 @@ export const deployTool = defineTool({
|
|
|
111
153
|
if ("out" in resolved)
|
|
112
154
|
return resolved.out;
|
|
113
155
|
const app = resolved.app;
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const scopeTypes = await listScopeTypes(context.np, app.nrn);
|
|
119
|
-
if (scopeTypes.length) {
|
|
120
|
-
const firstType = scopeTypes[0]?.type ?? scopeTypes[0]?.name ?? "";
|
|
121
|
-
const markdown = translate("deploy.noScopeTypes", {
|
|
122
|
-
types: scopeTypes.map((scopeType) => `**${scopeType.name}**`).join(", "),
|
|
123
|
-
}) + next(translate("deploy.createScopeHint", { type: firstType }));
|
|
124
|
-
return fail(markdown, { scope_types: scopeTypes });
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
return picked.out;
|
|
128
|
-
}
|
|
129
|
-
const scope = picked.scope;
|
|
156
|
+
const scopeResult = await resolveDeployScope(context, app, args.scope);
|
|
157
|
+
if ("out" in scopeResult)
|
|
158
|
+
return scopeResult.out;
|
|
159
|
+
const scope = scopeResult.scope;
|
|
130
160
|
const shipping = await resolveRelease(context, app.id, args);
|
|
131
161
|
if ("out" in shipping)
|
|
132
162
|
return shipping.out;
|
|
133
163
|
const { release, note } = shipping;
|
|
134
164
|
// Multi-asset builds must name the asset to ship; the platform's error otherwise is misleading.
|
|
135
|
-
|
|
136
|
-
if (
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const assets = await listAssets(context.np, buildId);
|
|
140
|
-
const choice = pickAsset(assets, scope.type);
|
|
141
|
-
if ("ambiguous" in choice) {
|
|
142
|
-
return reply(`${translate("deploy.multiAsset", { build: buildId, scope: scope.name, type: scope.type ?? "?" })}\n\n${table([translate("header.asset"), translate("header.type"), translate("header.platform")], choice.ambiguous.map((asset) => [asset.name, asset.type, asset.platform ?? ""]))}${next(translate("deploy.assetHint", { name: choice.ambiguous[0]?.name ?? "" }))}`, { assets: choice.ambiguous });
|
|
143
|
-
}
|
|
144
|
-
assetName = choice.name;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
165
|
+
const assetResult = await resolveDeployAsset(context, release, scope, args.asset);
|
|
166
|
+
if ("out" in assetResult)
|
|
167
|
+
return assetResult.out;
|
|
168
|
+
const assetName = assetResult.assetName;
|
|
147
169
|
const orgSlug = await context.org.organizationSlug();
|
|
148
170
|
// Convergent under retries: if this exact release is already rolling out on this scope
|
|
149
171
|
// (a non-terminal deployment), return that rollout instead of starting a second one.
|
|
@@ -13,7 +13,7 @@ import { appArg, offsetArg, pageOf, requireApp } from "./shared.js";
|
|
|
13
13
|
export const deploymentsTool = defineTool({
|
|
14
14
|
name: TOOL.applicationDeploymentList,
|
|
15
15
|
title: "Deployments",
|
|
16
|
-
description:
|
|
16
|
+
description: 'List ONE application\'s deployments across all its scopes, rendered as a PANEL — which release landed on which scope, the rollout status, and age (a deployment group landing one release on several scopes shows as one row). Use it for "deploy history", "what was deployed when", "find the active rollout". Does NOT deploy or change traffic (use application_deployment_create / application_deployment_update) and shows no logs/metrics. To find the newest deployment ACROSS apps, gather headlessly with entity_list (entity:"deployment") and compute it. Read-only.',
|
|
17
17
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
18
18
|
widget: "deployments",
|
|
19
19
|
errorKey: "deploymentList.errorLabel",
|
package/dist/tools/entity-get.js
CHANGED
|
@@ -21,7 +21,7 @@ const ENTITIES = [
|
|
|
21
21
|
export const entityGetTool = defineTool({
|
|
22
22
|
name: TOOL.entityGet,
|
|
23
23
|
title: "Read an entity (data)",
|
|
24
|
-
description: 'Read
|
|
24
|
+
description: 'Read ONE entity by id as DATA — returns structuredContent and renders NO panel (no widget). Use it to (a) drill into a specific entity while navigating headlessly with entity_list, or (b) POLL a changing status WITHOUT spamming panels — loop entity_get entity:"build" id:<id> (or "deployment") until the status is final, instead of re-calling application_get each time. Pass `entity` + `id` (e.g. entity:"application" id:123). Entities: organization, account, namespace, application, scope, build, release, deployment. Use the rendered reads (application_get/…) when the USER wants to SEE it; parameters/services/links/approvals have their own tools.',
|
|
25
25
|
annotations: { readOnlyHint: true },
|
|
26
26
|
// No widget on purpose: data-only. See CLAUDE.md (the data-only read pattern).
|
|
27
27
|
errorKey: "entityGet.errorLabel",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { getApplication, getScope, listApprovals, listEntity, listLinks, listParameters, listServices, } from "../np/journey.js";
|
|
2
|
+
import { getApplication, getScope, listApprovals, listEntity, listLinks, listParameters, listServices, listTemplates, } from "../np/journey.js";
|
|
3
3
|
import { defineTool, fail, reply } from "../tool.js";
|
|
4
4
|
import { TOOL } from "../tool-names.js";
|
|
5
5
|
/**
|
|
@@ -22,8 +22,9 @@ const REST_ENTITIES = [
|
|
|
22
22
|
"release",
|
|
23
23
|
"deployment",
|
|
24
24
|
];
|
|
25
|
-
/** NRN-addressed collections — listed by the parent's NRN (resolved from parent_id, or given as nrn).
|
|
26
|
-
|
|
25
|
+
/** NRN-addressed collections — listed by the parent's NRN (resolved from parent_id, or given as nrn).
|
|
26
|
+
* `template` is scoped by a NAMESPACE's NRN (the scaffolding templates available to a new app there). */
|
|
27
|
+
const NRN_ENTITIES = ["parameter", "service", "link", "approval", "template"];
|
|
27
28
|
const ENTITIES = [...REST_ENTITIES, ...NRN_ENTITIES];
|
|
28
29
|
/** The parents a collection can hang under — its id (REST) or its NRN (nrn-scoped) scopes the list. */
|
|
29
30
|
const PARENTS = ["organization", "account", "namespace", "application", "scope"];
|
|
@@ -38,6 +39,11 @@ async function resolveNrn(context, args) {
|
|
|
38
39
|
return (await getApplication(context.np, args.parent_id)).nrn;
|
|
39
40
|
if (args.parent_type === "scope")
|
|
40
41
|
return (await getScope(context.np, args.parent_id)).nrn;
|
|
42
|
+
if (args.parent_type === "namespace") {
|
|
43
|
+
// Templates hang off a namespace; resolve its NRN from the cached org skeleton (no extra fetch).
|
|
44
|
+
const skeleton = await context.org.getSkeleton();
|
|
45
|
+
return skeleton.namespaces.find((namespace) => namespace.id === args.parent_id)?.nrn;
|
|
46
|
+
}
|
|
41
47
|
return undefined;
|
|
42
48
|
}
|
|
43
49
|
/** List an nrn-scoped resource via its existing typed function (a different addressing scheme to REST). */
|
|
@@ -51,6 +57,8 @@ async function listByNrn(context, entity, nrn) {
|
|
|
51
57
|
return listLinks(context.np, nrn);
|
|
52
58
|
case "approval":
|
|
53
59
|
return listApprovals(context.bff, nrn);
|
|
60
|
+
case "template":
|
|
61
|
+
return listTemplates(context.np, nrn);
|
|
54
62
|
default:
|
|
55
63
|
return [];
|
|
56
64
|
}
|
|
@@ -58,14 +66,14 @@ async function listByNrn(context, entity, nrn) {
|
|
|
58
66
|
export const entityListTool = defineTool({
|
|
59
67
|
name: TOOL.entityList,
|
|
60
68
|
title: "List entities (data)",
|
|
61
|
-
description: '
|
|
69
|
+
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. `template` is scoped by a NAMESPACE: pass entity:"template" parent_type:"namespace" parent_id:<namespace_id> to list the scaffolding templates available for a new app there — do this to choose a template BEFORE opening the application_create form, so the form opens with it pre-selected (don\'t open the form blind to discover templates). For one entity by id use entity_get.',
|
|
62
70
|
annotations: { readOnlyHint: true },
|
|
63
71
|
// No widget on purpose: data-only navigation. See CLAUDE.md (the data-only read pattern).
|
|
64
72
|
errorKey: "entityList.errorLabel",
|
|
65
73
|
inputSchema: {
|
|
66
74
|
entity: z
|
|
67
75
|
.enum(ENTITIES)
|
|
68
|
-
.describe("Collection to list. REST tree: account, namespace, application, scope, build, release, deployment. NRN-scoped: parameter, service, link, approval."),
|
|
76
|
+
.describe("Collection to list. REST tree: account, namespace, application, scope, build, release, deployment. NRN-scoped: parameter, service, link, approval, template (template lists a namespace's scaffolding templates)."),
|
|
69
77
|
parent_type: z
|
|
70
78
|
.enum(PARENTS)
|
|
71
79
|
.optional()
|
|
@@ -74,7 +82,7 @@ export const entityListTool = defineTool({
|
|
|
74
82
|
nrn: z
|
|
75
83
|
.string()
|
|
76
84
|
.optional()
|
|
77
|
-
.describe(
|
|
85
|
+
.describe('For nrn-scoped entities (parameter/service/link/approval/template): the scoping NRN directly, if you have it; otherwise pass parent_type + parent_id (for template, parent_type:"namespace") and it\'s resolved for you.'),
|
|
78
86
|
limit: z.number().optional().describe("Max rows (default 100) — REST collections only."),
|
|
79
87
|
},
|
|
80
88
|
async handler(args, context) {
|
package/dist/tools/find-apps.js
CHANGED
|
@@ -8,13 +8,16 @@ import { TOOL } from "../tool-names.js";
|
|
|
8
8
|
export const findAppsTool = defineTool({
|
|
9
9
|
name: TOOL.applicationList,
|
|
10
10
|
title: "Applications",
|
|
11
|
-
description:
|
|
11
|
+
description: 'Search applications across the organization by partial name and/or namespace, rendered as a list PANEL grouped by namespace. Fast (parallel + cached). When the user asks for the apps IN a namespace ("my apps in pablov", "list the payments-ns apps"), ALWAYS pass `namespace` so the search is filtered at the source — never list the whole org and then filter the result yourself (the org can have hundreds of apps; post-filtering in code is wrong). Namespace matching is lenient (case- and space-insensitive: "pablov" matches "Pablo V"). Use `query` for a partial app name. Use this when the repo isn\'t linked or the user names an app/namespace you don\'t know.',
|
|
12
12
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
13
13
|
widget: "find-apps",
|
|
14
14
|
errorKey: "findApps.errorLabel",
|
|
15
15
|
inputSchema: {
|
|
16
16
|
query: z.string().optional().describe("Partial app name (case-insensitive). Omit to list everything."),
|
|
17
|
-
namespace: z
|
|
17
|
+
namespace: z
|
|
18
|
+
.string()
|
|
19
|
+
.optional()
|
|
20
|
+
.describe('Limit to a namespace by name — pass this whenever the user scopes to a namespace. Matched leniently (case- and space-insensitive): "pablov" or "pablo v" both match "Pablo V".'),
|
|
18
21
|
limit: z.number().optional(),
|
|
19
22
|
},
|
|
20
23
|
async handler(args, context) {
|
package/dist/tools/logs.js
CHANGED
|
@@ -1,14 +1,27 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { plural, translate } from "../i18n.js";
|
|
3
|
+
import { log } from "../log.js";
|
|
3
4
|
import { dashboardLink, linkLine, next } from "../md.js";
|
|
4
5
|
import { listScopes, readLogs } from "../np/journey.js";
|
|
5
6
|
import { defineTool, fail, reply } from "../tool.js";
|
|
6
7
|
import { TOOL } from "../tool-names.js";
|
|
7
8
|
import { appArg, chooseScope, requireApp } from "./shared.js";
|
|
9
|
+
const parseMs = (value) => {
|
|
10
|
+
if (!value)
|
|
11
|
+
return 0;
|
|
12
|
+
const ms = new Date(value).getTime();
|
|
13
|
+
return Number.isNaN(ms) ? 0 : ms;
|
|
14
|
+
};
|
|
15
|
+
/** A log entry's timestamp as epoch ms, from the platform's `date` field (ISO-8601). The single time
|
|
16
|
+
* source for BOTH ordering and the staleness check. Absent → 0 (sorts as oldest, adds no recency).
|
|
17
|
+
* We never parse a time out of the message TEXT: the platform already resolves each line's time into
|
|
18
|
+
* `date`; an app that logs structured JSON keeps its own `"time"` inside the message, which is the
|
|
19
|
+
* app's payload to show verbatim, not ours to reinterpret. */
|
|
20
|
+
const logTimeMs = (entry) => parseMs(entry.date);
|
|
8
21
|
export const logsTool = defineTool({
|
|
9
22
|
name: TOOL.applicationLogList,
|
|
10
23
|
title: "Logs",
|
|
11
|
-
description:
|
|
24
|
+
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", "show <app> logs". Pass `app` (name OR "#id") and this resolves the application ITSELF — call it DIRECTLY; do NOT call application_list (to find the app) or application_get (to look it up) first, that just renders panels the user didn\'t ask for. "show <app> logs" is ONE call: this tool with app:"<app>". Logs are per-scope: omit `scope` to use the only one (or be asked which). With no window the SERVER auto-widens 1m → 5m → 30m and returns the first window that has logs, then falls back to the LATEST available lines (any age) so a quiet/stopped app still shows its last logs (e.g. the crash) — it prefers recent activity but is NEVER empty, so you NEVER need to re-call with a wider window (don\'t widen client-side — one call is enough). To pin a specific range pass `start_time`/`end_time` (ISO-8601) or a bigger `lines` (an explicit window skips the auto-widen). Page older lines with `page_token`. Does NOT return metrics or deploy status — use application_metric_list or application_get. Read-only.',
|
|
12
25
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
13
26
|
widget: "logs",
|
|
14
27
|
errorKey: "logs.errorLabel",
|
|
@@ -46,14 +59,84 @@ export const logsTool = defineTool({
|
|
|
46
59
|
return picked.out;
|
|
47
60
|
const scope = picked.scope;
|
|
48
61
|
const maxLines = args.lines ?? 50;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
62
|
+
// No explicit window → progressively WIDEN the lookback (1m → 5m → 30m → 6h → 24h → 7d), stopping
|
|
63
|
+
// at the first window that actually has logs. So "show logs" starts with recent activity instead
|
|
64
|
+
// of an empty last-minute view on a quiet app. The wider steps past 30m are NOT cosmetic: an app
|
|
65
|
+
// that last logged, say, 45m ago has its newest lines just OUTSIDE the 30m "live" window. Without
|
|
66
|
+
// a 6h step the widen would skip straight to the unfiltered fallback — and an unfiltered read can
|
|
67
|
+
// return an OLDER partition than the most-recent lines (observed live: 06-19 lines surfaced while
|
|
68
|
+
// the app's real 06-22 lines sat in the 30m→unfiltered gap). An explicit `start_time` filters
|
|
69
|
+
// server-side, so the 6h step returns those 06-22 lines; staleness still flags them as not-live.
|
|
70
|
+
// The live tail re-runs this each poll. An explicit window / `page_token` skips the widening (the
|
|
71
|
+
// user/model asked for a specific range; widen further yourself to diagnose an older failure).
|
|
72
|
+
const explicit = Boolean(args.start_time || args.end_time || args.page_token);
|
|
73
|
+
const minute = 60_000;
|
|
74
|
+
const hour = 60 * minute;
|
|
75
|
+
const since = (ageMs) => ({ start_time: new Date(Date.now() - ageMs).toISOString() });
|
|
76
|
+
const windows = explicit
|
|
77
|
+
? [{ start_time: args.start_time, end_time: args.end_time, next_page_token: args.page_token }]
|
|
78
|
+
: [
|
|
79
|
+
since(minute),
|
|
80
|
+
since(5 * minute),
|
|
81
|
+
since(30 * minute),
|
|
82
|
+
since(6 * hour),
|
|
83
|
+
since(24 * hour),
|
|
84
|
+
since(7 * 24 * hour),
|
|
85
|
+
// Last resort: latest-available, ANY age — an app silent for >7d still shows its last lines
|
|
86
|
+
// (e.g. an old crash), NEVER an empty panel. The explicit windows above are what surface the
|
|
87
|
+
// REAL most-recent lines; an unfiltered read can return an older partition, so it's only the
|
|
88
|
+
// truly-ancient safety net, never the primary path.
|
|
89
|
+
{},
|
|
90
|
+
];
|
|
91
|
+
let page = await readLogs(context.np, { application_id: app.id, scope_id: scope.id, ...windows[0] });
|
|
92
|
+
let usedWindow = 0;
|
|
93
|
+
log.debug({ logsTool: { app: app.id, scope: scope.id, window: windows[0], count: page.results?.length ?? 0 } }, "logs: window 0");
|
|
94
|
+
for (let attempt = 1; attempt < windows.length && !(page.results ?? []).length; attempt++) {
|
|
95
|
+
page = await readLogs(context.np, { application_id: app.id, scope_id: scope.id, ...windows[attempt] });
|
|
96
|
+
usedWindow = attempt;
|
|
97
|
+
log.debug({
|
|
98
|
+
logsTool: {
|
|
99
|
+
app: app.id,
|
|
100
|
+
scope: scope.id,
|
|
101
|
+
window: windows[attempt],
|
|
102
|
+
count: page.results?.length ?? 0,
|
|
103
|
+
},
|
|
104
|
+
}, `logs: window ${attempt}`);
|
|
105
|
+
}
|
|
106
|
+
// Sort oldest → newest by timestamp, THEN take the newest `maxLines` (kept oldest-first for the
|
|
107
|
+
// "newest last" display). The platform returns lines newest-first, so a naive slice(-maxLines)
|
|
108
|
+
// took the OLDEST lines — which is why a multi-day log set surfaced days-old lines under "live".
|
|
109
|
+
const ordered = (page.results ?? []).slice().sort((left, right) => logTimeMs(left) - logTimeMs(right));
|
|
110
|
+
const entries = ordered.slice(-maxLines);
|
|
111
|
+
// Staleness: with no explicit window the auto-widen tries 1m → 5m → 30m before the latest-available
|
|
112
|
+
// fallback. So if the NEWEST line we got is older than that widest live window (30m), the app has had
|
|
113
|
+
// no recent activity and these are last-gasp lines (e.g. a crash days ago). Flag it so the panel/text
|
|
114
|
+
// says "no recent activity, last logs from <time>" instead of presenting old lines as a live tail.
|
|
115
|
+
// An explicit window/page_token means the user asked for that range — old lines there aren't "stale".
|
|
116
|
+
const newestMs = entries.reduce((max, entry) => Math.max(max, logTimeMs(entry)), 0);
|
|
117
|
+
const stale = !explicit && newestMs > 0 && Date.now() - newestMs > 30 * 60_000;
|
|
118
|
+
const newestTs = newestMs > 0 ? new Date(newestMs).toISOString() : null;
|
|
119
|
+
// The trace that pins down a logs-fetch complaint: which window won, how the staleness verdict was
|
|
120
|
+
// reached, and a sample of the RAW entries — the `date` field each line carries (the platform's
|
|
121
|
+
// timestamp) plus a short message head, so a missing/odd `date` is visible in the trace.
|
|
122
|
+
log.debug({
|
|
123
|
+
logsTool: {
|
|
124
|
+
app: app.id,
|
|
125
|
+
scope: scope.name,
|
|
126
|
+
explicit,
|
|
127
|
+
usedWindow,
|
|
128
|
+
rawCount: page.results?.length ?? 0,
|
|
129
|
+
entryCount: entries.length,
|
|
130
|
+
newestTs,
|
|
131
|
+
stale,
|
|
132
|
+
nowIso: new Date().toISOString(),
|
|
133
|
+
ageMin: newestMs > 0 ? Math.round((Date.now() - newestMs) / 60_000) : null,
|
|
134
|
+
sample: (page.results ?? []).slice(0, 6).map((entry) => ({
|
|
135
|
+
date: entry.date ?? null,
|
|
136
|
+
msgHead: entry.message.slice(0, 80),
|
|
137
|
+
})),
|
|
138
|
+
},
|
|
139
|
+
}, "logs: resolved entries + staleness");
|
|
57
140
|
const scopeSuffix = ` · ${scope.name}`;
|
|
58
141
|
// The full scope list rides along so the widget can offer a scope switcher.
|
|
59
142
|
const scopeList = scopes.map((candidate) => ({ name: candidate.name, status: candidate.status }));
|
|
@@ -64,24 +147,38 @@ export const logsTool = defineTool({
|
|
|
64
147
|
}
|
|
65
148
|
const body = entries
|
|
66
149
|
.map((entry) => {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
return `${prefix}${
|
|
150
|
+
// Timestamp column comes from the `date` field; guard a bad value (the API could return an
|
|
151
|
+
// unparseable date) so the prefix is just dropped rather than throwing.
|
|
152
|
+
const ms = parseMs(entry.date);
|
|
153
|
+
const prefix = ms ? `${new Date(ms).toISOString().slice(5, 19).replace("T", " ")} ` : "";
|
|
154
|
+
return `${prefix}${entry.message.trimEnd()}`;
|
|
72
155
|
})
|
|
73
156
|
.join("\n");
|
|
74
|
-
const
|
|
157
|
+
const heading = stale
|
|
158
|
+
? plural(entries.length, "logs.staleLines.one", "logs.staleLines.many", {
|
|
159
|
+
app: app.name,
|
|
160
|
+
scope: scopeSuffix,
|
|
161
|
+
time: (newestTs ?? "").slice(0, 16).replace("T", " "),
|
|
162
|
+
})
|
|
163
|
+
: plural(entries.length, "logs.lastLines.one", "logs.lastLines.many", {
|
|
164
|
+
app: app.name,
|
|
165
|
+
scope: scopeSuffix,
|
|
166
|
+
});
|
|
167
|
+
const markdown = `${heading}\n\n\`\`\`\n${body}\n\`\`\``;
|
|
75
168
|
return reply(markdown, {
|
|
76
169
|
count: entries.length,
|
|
170
|
+
stale,
|
|
171
|
+
newest_ts: newestTs,
|
|
77
172
|
next_page_token: page.next_page_token ?? null,
|
|
78
173
|
app: `#${app.id}`,
|
|
79
174
|
app_name: app.name,
|
|
80
175
|
scope: scope.name,
|
|
81
176
|
scopes: scopeList,
|
|
177
|
+
// Two columns: the timestamp from the `date` field (null → the widget shows no timestamp cell,
|
|
178
|
+
// just the message), and the message verbatim. No parsing time out of the text.
|
|
82
179
|
lines: entries.map((entry) => ({
|
|
83
|
-
ts: entry.
|
|
84
|
-
message: entry.message
|
|
180
|
+
ts: entry.date ?? null,
|
|
181
|
+
message: entry.message,
|
|
85
182
|
})),
|
|
86
183
|
});
|
|
87
184
|
},
|
package/dist/tools/metrics.js
CHANGED
|
@@ -8,7 +8,7 @@ import { appArg, chooseScope, requireApp } from "./shared.js";
|
|
|
8
8
|
export const metricsTool = defineTool({
|
|
9
9
|
name: TOOL.applicationMetricList,
|
|
10
10
|
title: "Performance metrics",
|
|
11
|
-
description:
|
|
11
|
+
description: 'The golden signals of ONE scope — throughput (rpm), response time, error rate, CPU, memory — with sparkline trends, rendered as a metrics PANEL. Use it for "how is it performing?", "is it healthy?", or to watch a scope during a rollout. Pick the `window` (1h/3h/24h/7d, default 3h); omit `scope` to use the only one. Does NOT return logs or deploy state — use application_log_list or application_get. Read-only.',
|
|
12
12
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
13
13
|
widget: "metrics",
|
|
14
14
|
errorKey: "metrics.errorLabel",
|