@rse/ase 0.0.40 → 0.0.41
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/dst/ase-config.js +2 -0
- package/dst/ase-decision.js +67 -0
- package/dst/ase-service.js +25 -26
- package/dst/ase-skills.js +224 -0
- package/package.json +6 -4
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.github/plugin/plugin.json +1 -1
- package/plugin/package.json +1 -1
- package/plugin/skills/ase-arch-analyze/SKILL.md +0 -1
- package/plugin/skills/ase-arch-discover/SKILL.md +44 -32
- package/plugin/skills/ase-code-analyze/SKILL.md +0 -1
- package/plugin/skills/ase-code-craft/SKILL.md +0 -1
- package/plugin/skills/ase-code-explain/SKILL.md +0 -1
- package/plugin/skills/ase-code-insight/SKILL.md +0 -1
- package/plugin/skills/ase-code-lint/SKILL.md +0 -1
- package/plugin/skills/ase-code-refactor/SKILL.md +0 -1
- package/plugin/skills/ase-code-resolve/SKILL.md +0 -1
- package/plugin/skills/ase-meta-changes/SKILL.md +0 -1
- package/plugin/skills/ase-meta-chat/SKILL.md +0 -1
- package/plugin/skills/ase-meta-commit/SKILL.md +0 -1
- package/plugin/skills/ase-meta-evaluate/SKILL.md +10 -8
- package/plugin/skills/ase-meta-quorum/SKILL.md +0 -1
- package/plugin/skills/ase-meta-search/SKILL.md +0 -1
- package/plugin/skills/ase-meta-why/SKILL.md +0 -1
- package/plugin/skills/ase-task-delete/SKILL.md +0 -1
- package/plugin/skills/ase-task-edit/SKILL.md +0 -1
- package/plugin/skills/ase-task-id/SKILL.md +0 -1
- package/plugin/skills/ase-task-implement/SKILL.md +0 -1
- package/plugin/skills/ase-task-list/SKILL.md +0 -1
- package/plugin/skills/ase-task-preflight/SKILL.md +0 -1
- package/plugin/skills/ase-task-reboot/SKILL.md +0 -1
- package/plugin/skills/ase-task-view/SKILL.md +0 -1
package/dst/ase-config.js
CHANGED
|
@@ -485,6 +485,8 @@ export class Config {
|
|
|
485
485
|
delete(key) {
|
|
486
486
|
this.assertWritable(key);
|
|
487
487
|
const td = this.docs[this.target];
|
|
488
|
+
if (td.doc.contents === null || td.doc.contents === undefined)
|
|
489
|
+
return;
|
|
488
490
|
const next = td.doc.clone();
|
|
489
491
|
next.deleteIn(this.resolveKey(key).split("."));
|
|
490
492
|
const saved = td.doc;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/*
|
|
2
|
+
** Agentic Software Engineering (ASE)
|
|
3
|
+
** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
|
+
** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
|
|
5
|
+
*/
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
/* reusable functionality: weighted multi-criteria decision matrix */
|
|
8
|
+
export class Decision {
|
|
9
|
+
/* compute the per-alternative product-sum (rating) row from a
|
|
10
|
+
weighted decision matrix. Each input row has the shape
|
|
11
|
+
`[weight, eval_1, eval_2, ..., eval_N]`. For each alternative
|
|
12
|
+
column j (1..N), the result is the sum over all rows K of
|
|
13
|
+
`weight_K * eval_K_j`. The output array has length N. */
|
|
14
|
+
static productSum(matrix) {
|
|
15
|
+
if (matrix.length === 0)
|
|
16
|
+
return [];
|
|
17
|
+
const cols = matrix[0].length;
|
|
18
|
+
if (cols < 2)
|
|
19
|
+
throw new Error("each row must contain a weight followed by at least one evaluation column");
|
|
20
|
+
const N = cols - 1;
|
|
21
|
+
const ratings = new Array(N).fill(0);
|
|
22
|
+
for (let i = 0; i < matrix.length; i++) {
|
|
23
|
+
const row = matrix[i];
|
|
24
|
+
if (row.length !== cols)
|
|
25
|
+
throw new Error(`row ${i} has ${row.length} columns, expected ${cols}`);
|
|
26
|
+
const weight = row[0];
|
|
27
|
+
for (let j = 0; j < N; j++)
|
|
28
|
+
ratings[j] += weight * row[j + 1];
|
|
29
|
+
}
|
|
30
|
+
return ratings;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/* MCP registration entry point for the decision-matrix tool */
|
|
34
|
+
export class DecisionMCP {
|
|
35
|
+
register(mcp) {
|
|
36
|
+
mcp.registerTool("decision_matrix", {
|
|
37
|
+
title: "ASE decision matrix",
|
|
38
|
+
description: "Compute the per-alternative product-sum (rating) row of a weighted " +
|
|
39
|
+
"multi-criteria decision matrix. The input `matrix` is an array of rows, " +
|
|
40
|
+
"one row per criterion, where each row has the shape " +
|
|
41
|
+
"`[weight, eval_1, eval_2, ..., eval_N]` (i.e. the criterion weight " +
|
|
42
|
+
"followed by N evaluation values, one per alternative). For each " +
|
|
43
|
+
"alternative column j (1..N), the result is the sum over all rows K of " +
|
|
44
|
+
"`weight_K * eval_K_j`. Returns a JSON `text` array of length N with " +
|
|
45
|
+
"the raw, unrounded ratings (one per alternative, in the same column " +
|
|
46
|
+
"order as the input).",
|
|
47
|
+
inputSchema: {
|
|
48
|
+
matrix: z.array(z.array(z.number()))
|
|
49
|
+
.describe("Decision matrix rows: each row is `[weight, eval_1, ..., eval_N]`")
|
|
50
|
+
}
|
|
51
|
+
}, async (args) => {
|
|
52
|
+
try {
|
|
53
|
+
const result = Decision.productSum(args.matrix);
|
|
54
|
+
return {
|
|
55
|
+
content: [{ type: "text", text: JSON.stringify(result) }]
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
60
|
+
return {
|
|
61
|
+
isError: true,
|
|
62
|
+
content: [{ type: "text", text: `ERROR: ${message}` }]
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
package/dst/ase-service.js
CHANGED
|
@@ -9,7 +9,7 @@ import net from "node:net";
|
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
import { spawn } from "node:child_process";
|
|
11
11
|
import Hapi from "@hapi/hapi";
|
|
12
|
-
import
|
|
12
|
+
import { ofetch } from "ofetch";
|
|
13
13
|
import { isMap } from "yaml";
|
|
14
14
|
import prettyMs from "pretty-ms";
|
|
15
15
|
import * as v from "valibot";
|
|
@@ -22,6 +22,7 @@ import { KVMCP } from "./ase-kv.js";
|
|
|
22
22
|
import PersonaMCP from "./ase-persona.js";
|
|
23
23
|
import { TimestampMCP } from "./ase-timestamp.js";
|
|
24
24
|
import { GetoptMCP } from "./ase-getopt.js";
|
|
25
|
+
import { SkillsMCP } from "./ase-skills.js";
|
|
25
26
|
import pkg from "../package.json" with { type: "json" };
|
|
26
27
|
/* shared service host */
|
|
27
28
|
export const SERVICE_HOST = "127.0.0.1";
|
|
@@ -30,23 +31,24 @@ const HOST = SERVICE_HOST;
|
|
|
30
31
|
export const serviceSchema = v.nullish(v.strictObject({
|
|
31
32
|
port: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1024), v.maxValue(65535)))
|
|
32
33
|
}));
|
|
33
|
-
/* distinguish ECONNREFUSED from other
|
|
34
|
+
/* distinguish ECONNREFUSED from other ofetch transport errors */
|
|
34
35
|
export const isConnRefused = (err) => {
|
|
35
36
|
const e = err;
|
|
36
|
-
return e?.code === "ECONNREFUSED"
|
|
37
|
+
return e?.code === "ECONNREFUSED"
|
|
38
|
+
|| e?.cause?.code === "ECONNREFUSED"
|
|
39
|
+
|| e?.cause?.cause?.code === "ECONNREFUSED";
|
|
37
40
|
};
|
|
38
41
|
/* probe the service and verify ASE identity banner */
|
|
39
42
|
export const probe = async (port, projectId) => {
|
|
40
43
|
try {
|
|
41
|
-
const r = await
|
|
44
|
+
const r = await ofetch.raw(`http://${SERVICE_HOST}:${port}/`, {
|
|
42
45
|
method: "OPTIONS",
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
validateStatus: () => true
|
|
46
|
+
signal: AbortSignal.timeout(2000),
|
|
47
|
+
ignoreResponseError: true
|
|
46
48
|
});
|
|
47
49
|
if (r.status < 200 || r.status >= 300)
|
|
48
50
|
return false;
|
|
49
|
-
const d = r.
|
|
51
|
+
const d = r._data;
|
|
50
52
|
return d?.ase === true && d?.projectId === projectId;
|
|
51
53
|
}
|
|
52
54
|
catch (err) {
|
|
@@ -238,6 +240,7 @@ export default class ServiceCommand {
|
|
|
238
240
|
new PersonaMCP(this.log).register(mcp);
|
|
239
241
|
new TimestampMCP().register(mcp);
|
|
240
242
|
new GetoptMCP().register(mcp);
|
|
243
|
+
new SkillsMCP().register(mcp);
|
|
241
244
|
return mcp;
|
|
242
245
|
};
|
|
243
246
|
/* listen to HTTP/REST endpoints */
|
|
@@ -466,15 +469,14 @@ export default class ServiceCommand {
|
|
|
466
469
|
}
|
|
467
470
|
const match = await probe(ctx.port, ctx.projectId);
|
|
468
471
|
if (match === true) {
|
|
469
|
-
const r = await
|
|
472
|
+
const r = await ofetch.raw(`http://${HOST}:${ctx.port}/command`, {
|
|
470
473
|
method: "POST",
|
|
471
|
-
url: `http://${HOST}:${ctx.port}/command`,
|
|
472
474
|
headers: { "Content-Type": "application/json" },
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
475
|
+
body: { command: "status" },
|
|
476
|
+
signal: AbortSignal.timeout(2000),
|
|
477
|
+
ignoreResponseError: true
|
|
476
478
|
});
|
|
477
|
-
const d = r.
|
|
479
|
+
const d = r._data;
|
|
478
480
|
const uptimeMs = typeof d?.uptimeMs === "number" ? d.uptimeMs : 0;
|
|
479
481
|
const uptime = prettyMs(uptimeMs, { verbose: true });
|
|
480
482
|
process.stdout.write(`service: running on port ${ctx.port} (uptime: ${uptime})\n`);
|
|
@@ -503,17 +505,15 @@ export default class ServiceCommand {
|
|
|
503
505
|
if (ctx.port === null)
|
|
504
506
|
throw new Error("service not running (no port configured after auto-start)");
|
|
505
507
|
}
|
|
506
|
-
const r = await
|
|
508
|
+
const r = await ofetch.raw(`http://${HOST}:${ctx.port}/command`, {
|
|
507
509
|
method: "POST",
|
|
508
|
-
url: `http://${HOST}:${ctx.port}/command`,
|
|
509
510
|
headers: { "Content-Type": "application/json" },
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
responseType: "text"
|
|
514
|
-
transformResponse: [(x) => x]
|
|
511
|
+
body: { command: cmd },
|
|
512
|
+
signal: AbortSignal.timeout(5000),
|
|
513
|
+
ignoreResponseError: true,
|
|
514
|
+
responseType: "text"
|
|
515
515
|
});
|
|
516
|
-
const body = typeof r.
|
|
516
|
+
const body = typeof r._data === "string" ? r._data : JSON.stringify(r._data);
|
|
517
517
|
process.stdout.write(body);
|
|
518
518
|
if (!body.endsWith("\n"))
|
|
519
519
|
process.stdout.write("\n");
|
|
@@ -536,11 +536,10 @@ export default class ServiceCommand {
|
|
|
536
536
|
Service.clearPort(ctx.svc);
|
|
537
537
|
return 0;
|
|
538
538
|
}
|
|
539
|
-
const r = await
|
|
539
|
+
const r = await ofetch.raw(`http://${HOST}:${ctx.port}/stop`, {
|
|
540
540
|
method: "GET",
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
validateStatus: () => true
|
|
541
|
+
signal: AbortSignal.timeout(5000),
|
|
542
|
+
ignoreResponseError: true
|
|
544
543
|
});
|
|
545
544
|
const ok = r.status >= 200 && r.status < 300;
|
|
546
545
|
Service.clearPort(ctx.svc);
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/*
|
|
2
|
+
** Agentic Software Engineering (ASE)
|
|
3
|
+
** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
|
+
** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
|
|
5
|
+
*/
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { ofetch } from "ofetch";
|
|
8
|
+
import pacote from "pacote";
|
|
9
|
+
/* reusable functionality: gather per-package metadata in maximum parallel */
|
|
10
|
+
export class Skills {
|
|
11
|
+
/* HTTP timeout for the GitHub/npm-downloads side calls */
|
|
12
|
+
static HTTP_TIMEOUT_MS = 10_000;
|
|
13
|
+
/* cap concurrent ofetch web requests to avoid hammering the remote
|
|
14
|
+
endpoints (GitHub API, npm downloads API) */
|
|
15
|
+
static HTTP_CONCURRENCY = 4;
|
|
16
|
+
static httpActive = 0;
|
|
17
|
+
static httpQueue = [];
|
|
18
|
+
static async httpLimit(fn) {
|
|
19
|
+
if (Skills.httpActive >= Skills.HTTP_CONCURRENCY)
|
|
20
|
+
await new Promise((resolve) => Skills.httpQueue.push(resolve));
|
|
21
|
+
Skills.httpActive++;
|
|
22
|
+
try {
|
|
23
|
+
return await fn();
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
Skills.httpActive--;
|
|
27
|
+
const next = Skills.httpQueue.shift();
|
|
28
|
+
if (next !== undefined)
|
|
29
|
+
next();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/* fetch the full registry packument for a single package */
|
|
33
|
+
static async fetchPackument(name) {
|
|
34
|
+
try {
|
|
35
|
+
const pkg = await pacote.packument(name, { fullMetadata: true });
|
|
36
|
+
const version = pkg["dist-tags"]?.latest ?? "";
|
|
37
|
+
const time = pkg.time ?? {};
|
|
38
|
+
const verEntry = version !== "" ? pkg.versions?.[version] : undefined;
|
|
39
|
+
let repository = "";
|
|
40
|
+
if (verEntry !== undefined) {
|
|
41
|
+
const r = verEntry.repository;
|
|
42
|
+
if (typeof r === "string")
|
|
43
|
+
repository = r;
|
|
44
|
+
else if (r !== undefined && typeof r.url === "string")
|
|
45
|
+
repository = r.url;
|
|
46
|
+
}
|
|
47
|
+
return { version, time, repository };
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return { version: "", time: {}, repository: "" };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/* fetch GitHub stars given a repository URL (or empty string) */
|
|
54
|
+
static async fetchStars(repository) {
|
|
55
|
+
const m = /^.+?\/\/github\.com\/([^/]+\/[^/#?]+?)(?:\.git)?(?:[/#?].*)?$/.exec(repository);
|
|
56
|
+
if (m === null)
|
|
57
|
+
return "N.A.";
|
|
58
|
+
try {
|
|
59
|
+
const data = await Skills.httpLimit(() => ofetch(`https://api.github.com/repos/${m[1]}`, { timeout: Skills.HTTP_TIMEOUT_MS, ignoreResponseError: true }));
|
|
60
|
+
const n = data?.stargazers_count;
|
|
61
|
+
return typeof n === "number" ? n : "N.A.";
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return "N.A.";
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/* fetch last-month npm downloads for a single package */
|
|
68
|
+
static async fetchDownloads(name) {
|
|
69
|
+
try {
|
|
70
|
+
const data = await Skills.httpLimit(() => ofetch(`https://api.npmjs.org/downloads/point/last-month/${encodeURIComponent(name)}`, { timeout: Skills.HTTP_TIMEOUT_MS, ignoreResponseError: true }));
|
|
71
|
+
const n = data?.downloads;
|
|
72
|
+
return typeof n === "number" ? n : "N.A.";
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return "N.A.";
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/* gather metadata for all given components in maximum parallel,
|
|
79
|
+
dispatching on the technology stack:
|
|
80
|
+
- "JavaScript"/"TypeScript": NPM registry (pacote) + GitHub stars + npm-downloads
|
|
81
|
+
- "Java"/"Kotlin"/"Unknown": not yet supported -- return empty result */
|
|
82
|
+
static async info(stack, components) {
|
|
83
|
+
/* FIXME: currently just limited technology stack support */
|
|
84
|
+
if (stack !== "JavaScript" && stack !== "TypeScript")
|
|
85
|
+
return [];
|
|
86
|
+
/* per package: kick off packument and downloads in parallel,
|
|
87
|
+
then stars as soon as the packument resolves; across packages
|
|
88
|
+
everything runs concurrently via Promise.all */
|
|
89
|
+
const results = await Promise.all(components.map(async (name) => {
|
|
90
|
+
const packumentPromise = Skills.fetchPackument(name);
|
|
91
|
+
const downloadsPromise = Skills.fetchDownloads(name);
|
|
92
|
+
const starsPromise = packumentPromise.then((p) => Skills.fetchStars(p.repository));
|
|
93
|
+
const [p, downloads, stars] = await Promise.all([
|
|
94
|
+
packumentPromise, downloadsPromise, starsPromise
|
|
95
|
+
]);
|
|
96
|
+
const created = p.time.created ?? "";
|
|
97
|
+
const updated = p.version !== "" ? (p.time[p.version] ?? "") : "";
|
|
98
|
+
const rank = Skills.computeRank(downloads, stars, created, updated);
|
|
99
|
+
return {
|
|
100
|
+
name,
|
|
101
|
+
version: p.version,
|
|
102
|
+
created,
|
|
103
|
+
updated,
|
|
104
|
+
repository: p.repository,
|
|
105
|
+
stars,
|
|
106
|
+
downloads,
|
|
107
|
+
rank
|
|
108
|
+
};
|
|
109
|
+
}));
|
|
110
|
+
/* sort by rank in descending order (best first) */
|
|
111
|
+
results.sort((a, b) => b.rank - a.rank);
|
|
112
|
+
return results;
|
|
113
|
+
}
|
|
114
|
+
/* compute composite rank score from weighted metrics:
|
|
115
|
+
downloads x
|
|
116
|
+
stars x
|
|
117
|
+
([lifespan =] (updated - created)) x
|
|
118
|
+
([recentness =] exp(-(now - updated) / halfLife)) */
|
|
119
|
+
static computeRank(downloads, stars, created, updated) {
|
|
120
|
+
const d = typeof downloads === "number" ? downloads : 0;
|
|
121
|
+
const s = typeof stars === "number" ? stars : 0;
|
|
122
|
+
const cMs = created !== "" ? Date.parse(created) : NaN;
|
|
123
|
+
const uMs = updated !== "" ? Date.parse(updated) : NaN;
|
|
124
|
+
if (Number.isNaN(cMs) || Number.isNaN(uMs))
|
|
125
|
+
return 0;
|
|
126
|
+
const now = Date.now();
|
|
127
|
+
const msPerDay = 1000 * 60 * 60 * 24;
|
|
128
|
+
const halfLife = 365 / 2;
|
|
129
|
+
const lifespan = Math.max(0, uMs - cMs);
|
|
130
|
+
const ageDays = Math.max(0, (now - uMs) / msPerDay);
|
|
131
|
+
const recentness = Math.exp(-ageDays / halfLife);
|
|
132
|
+
return d * s * lifespan * recentness;
|
|
133
|
+
}
|
|
134
|
+
/* compute the per-alternative product-sum (rating) row from a
|
|
135
|
+
weighted decision matrix. Each input row has the shape
|
|
136
|
+
`[ weight, eval_1, eval_2, ..., eval_N ]`. For each alternative
|
|
137
|
+
column j (1..N), the result is the sum over all rows K of
|
|
138
|
+
`weight_K * eval_K_j`. The output array has length N. */
|
|
139
|
+
static productSum(matrix) {
|
|
140
|
+
if (matrix.length === 0)
|
|
141
|
+
return [];
|
|
142
|
+
const cols = matrix[0].length;
|
|
143
|
+
if (cols < 2)
|
|
144
|
+
throw new Error("each row must contain a weight followed by at least one evaluation column");
|
|
145
|
+
const N = cols - 1;
|
|
146
|
+
const ratings = new Array(N).fill(0);
|
|
147
|
+
for (let i = 0; i < matrix.length; i++) {
|
|
148
|
+
const row = matrix[i];
|
|
149
|
+
if (row.length !== cols)
|
|
150
|
+
throw new Error(`row ${i} has ${row.length} columns, expected ${cols}`);
|
|
151
|
+
const weight = row[0];
|
|
152
|
+
for (let j = 0; j < N; j++)
|
|
153
|
+
ratings[j] += weight * row[j + 1];
|
|
154
|
+
}
|
|
155
|
+
return ratings;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/* MCP registration entry point for various skill helper tools */
|
|
159
|
+
export class SkillsMCP {
|
|
160
|
+
register(mcp) {
|
|
161
|
+
mcp.registerTool("component_info", {
|
|
162
|
+
title: "ASE component info",
|
|
163
|
+
description: "Gather metadata for a list of NPM packages in maximum parallel. " +
|
|
164
|
+
"For each package, fetches the full registry packument via `pacote` " +
|
|
165
|
+
"(in-process, no `npm view` subprocess), the GitHub `stargazers_count` " +
|
|
166
|
+
"from `api.github.com` (if the repository points to GitHub), and the " +
|
|
167
|
+
"last-month downloads from `api.npmjs.org`. Returns a JSON `text` array " +
|
|
168
|
+
"of `{ name, version, created, updated, repository, stars, downloads, rank }` " +
|
|
169
|
+
"objects, sorted in descending order by `rank`. Failures of " +
|
|
170
|
+
"individual side calls are isolated and reported as `\"N.A.\"` or empty " +
|
|
171
|
+
"string so every entry has the full shape.",
|
|
172
|
+
inputSchema: {
|
|
173
|
+
stack: z.string()
|
|
174
|
+
.describe("Technology stack: \"JavaScript\", \"TypeScript\", \"Java\", \"Kotlin\", or \"Unknown\""),
|
|
175
|
+
components: z.array(z.string())
|
|
176
|
+
.describe("List of package names to gather metadata for")
|
|
177
|
+
}
|
|
178
|
+
}, async (args) => {
|
|
179
|
+
try {
|
|
180
|
+
const result = await Skills.info(args.stack, args.components);
|
|
181
|
+
return {
|
|
182
|
+
content: [{ type: "text", text: JSON.stringify(result) }]
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
187
|
+
return {
|
|
188
|
+
isError: true,
|
|
189
|
+
content: [{ type: "text", text: `ERROR: ${message}` }]
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
mcp.registerTool("decision_matrix", {
|
|
194
|
+
title: "ASE decision matrix",
|
|
195
|
+
description: "Compute the per-alternative product-sum (rating) row of a weighted " +
|
|
196
|
+
"multi-criteria decision matrix. The input `matrix` is an array of rows, " +
|
|
197
|
+
"one row per criterion, where each row has the shape " +
|
|
198
|
+
"`[ weight, eval_1, eval_2, ..., eval_N ]` (i.e. the criterion weight " +
|
|
199
|
+
"followed by N evaluation values, one per alternative). For each " +
|
|
200
|
+
"alternative column j (1..N), the result is the sum over all rows K of " +
|
|
201
|
+
"`weight_K * eval_K_j`. Returns a JSON `text` array of length N with " +
|
|
202
|
+
"the raw, unrounded ratings (one per alternative, in the same column " +
|
|
203
|
+
"order as the input).",
|
|
204
|
+
inputSchema: {
|
|
205
|
+
matrix: z.array(z.array(z.number()))
|
|
206
|
+
.describe("Decision matrix rows: each row is `[weight, eval_1, ..., eval_N]`")
|
|
207
|
+
}
|
|
208
|
+
}, async (args) => {
|
|
209
|
+
try {
|
|
210
|
+
const result = Skills.productSum(args.matrix);
|
|
211
|
+
return {
|
|
212
|
+
content: [{ type: "text", text: JSON.stringify(result) }]
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
217
|
+
return {
|
|
218
|
+
isError: true,
|
|
219
|
+
content: [{ type: "text", text: `ERROR: ${message}` }]
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"homepage": "http://github.com/rse/ase",
|
|
7
7
|
"repository": { "url": "git+https://github.com/rse/ase.git", "type": "git" },
|
|
8
8
|
"bugs": { "url": "http://github.com/rse/ase/issues" },
|
|
9
|
-
"version": "0.0.
|
|
9
|
+
"version": "0.0.41",
|
|
10
10
|
"license": "GPL-3.0-only",
|
|
11
11
|
"author": {
|
|
12
12
|
"name": "Dr. Ralf S. Engelschall",
|
|
@@ -36,7 +36,8 @@
|
|
|
36
36
|
"@types/update-notifier": "6.0.8",
|
|
37
37
|
"@types/shell-quote": "1.7.5",
|
|
38
38
|
"@types/proper-lockfile": "4.1.4",
|
|
39
|
-
"@types/write-file-atomic": "4.0.3"
|
|
39
|
+
"@types/write-file-atomic": "4.0.3",
|
|
40
|
+
"@types/pacote": "11.1.8"
|
|
40
41
|
},
|
|
41
42
|
"dependencies": {
|
|
42
43
|
"commander": "14.0.3",
|
|
@@ -45,7 +46,6 @@
|
|
|
45
46
|
"execa": "9.6.1",
|
|
46
47
|
"mkdirp": "3.0.1",
|
|
47
48
|
"@hapi/hapi": "21.4.9",
|
|
48
|
-
"axios": "1.16.1",
|
|
49
49
|
"beautiful-mermaid": "1.1.3",
|
|
50
50
|
"cli-table3": "0.6.5",
|
|
51
51
|
"chalk": "5.6.2",
|
|
@@ -57,7 +57,9 @@
|
|
|
57
57
|
"update-notifier": "7.3.1",
|
|
58
58
|
"shell-quote": "1.8.3",
|
|
59
59
|
"proper-lockfile": "4.1.2",
|
|
60
|
-
"write-file-atomic": "4.0.0"
|
|
60
|
+
"write-file-atomic": "4.0.0",
|
|
61
|
+
"pacote": "21.5.0",
|
|
62
|
+
"ofetch": "1.4.1"
|
|
61
63
|
},
|
|
62
64
|
"engines": {
|
|
63
65
|
"npm": ">=10.0.0",
|
package/plugin/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"homepage": "http://github.com/rse/ase",
|
|
7
7
|
"repository": { "url": "git+https://github.com/rse/ase.git", "type": "git" },
|
|
8
8
|
"bugs": { "url": "http://github.com/rse/ase/issues" },
|
|
9
|
-
"version": "0.0.
|
|
9
|
+
"version": "0.0.41",
|
|
10
10
|
"license": "GPL-3.0-only",
|
|
11
11
|
"author": {
|
|
12
12
|
"name": "Dr. Ralf S. Engelschall",
|
|
@@ -9,14 +9,11 @@ disable-model-invocation: false
|
|
|
9
9
|
effort: medium
|
|
10
10
|
allowed-tools:
|
|
11
11
|
- "Bash(npm search --json *)"
|
|
12
|
-
- "Bash(npm view --json *)"
|
|
13
12
|
- "Skill"
|
|
14
13
|
- "Agent"
|
|
15
|
-
- "WebFetch"
|
|
16
14
|
---
|
|
17
15
|
|
|
18
16
|
@${CLAUDE_SKILL_DIR}/../../meta/ase-control.md
|
|
19
|
-
@${CLAUDE_SKILL_DIR}/../../meta/ase-persona.md
|
|
20
17
|
@${CLAUDE_SKILL_DIR}/../../meta/ase-skill.md
|
|
21
18
|
|
|
22
19
|
Discover Components
|
|
@@ -113,26 +110,6 @@ for the technology stack to *provide* the *needed functionality*
|
|
|
113
110
|
(L=1-M). Merge the results into the already existing result
|
|
114
111
|
set, but deduplicate entries.
|
|
115
112
|
|
|
116
|
-
- For each discovered *NPM package* <component-K/> (K=1-N),
|
|
117
|
-
use the shell command `npm view --json "<package-K/>"
|
|
118
|
-
version time repository.url` to discover
|
|
119
|
-
its version <version-K/>, the publish time of that
|
|
120
|
-
version <updated-K/> (read from `time[<version-K/>]`),
|
|
121
|
-
its time created <created-K/> (read from `time.created`),
|
|
122
|
-
and its repository URL <repository-K/>.
|
|
123
|
-
|
|
124
|
-
- If the <repository-K/> regexp-matches
|
|
125
|
-
`.+?//github\.com/([^/]+/[^/.]+).*` use the `WebFetch` tool
|
|
126
|
-
to fetch the URL `https://api.github.com/repos/$1` (`$1`
|
|
127
|
-
is the value matched by the first capturing parenthesis
|
|
128
|
-
in the regexp) and extract <stars-K/> from its JSON
|
|
129
|
-
`stargazers_count` field, else set <stars-K/> to `N.A.`.
|
|
130
|
-
|
|
131
|
-
- For each discovered *NPM package* <component-K/>
|
|
132
|
-
(K=1-N), use the `WebFetch` tool on the URL
|
|
133
|
-
`https://api.npmjs.org/downloads/point/last-month/<package-K/>`
|
|
134
|
-
to extract the <downloads-K/> from the `downloads` field.
|
|
135
|
-
|
|
136
113
|
- If <stack/> is "Java" or "Kotlin":
|
|
137
114
|
|
|
138
115
|
- Based on the essential keywords <keyword-L/> (L=1-M),
|
|
@@ -140,25 +117,60 @@ for the technology stack to *provide* the *needed functionality*
|
|
|
140
117
|
discover an initial set of a maximum of 12 *Java packages*
|
|
141
118
|
<component-K/> and at least their real name <name-K/> and
|
|
142
119
|
their unique package names <package-K/>.
|
|
120
|
+
|
|
121
|
+
- Call the `component_info(stack: <stack/>, components:
|
|
122
|
+
[ <package-1/>, ..., <package-N/> ])` tool of the `ase` MCP
|
|
123
|
+
service *once* for the entire set of discovered packages.
|
|
124
|
+
The tool dispatches internally on <stack/> and fetches all
|
|
125
|
+
metadata in maximum parallel and returns an array of objects `{
|
|
126
|
+
name, version, time, repository, stars, downloads }`. For each
|
|
127
|
+
component <component-K/> (K=1-N) read from its corresponding
|
|
128
|
+
entry: <version-K/> from `version`, <updated-K/> from `updated`,
|
|
129
|
+
<created-K/> from `created`, <repository-K/> from `repository`,
|
|
130
|
+
<stars-K/> from `stars` (numeric or `N.A.`), <downloads-K/>
|
|
131
|
+
from `downloads` (numeric or `N.A.`) and <rank-K/> from `rank`
|
|
132
|
+
(numeric).
|
|
133
|
+
|
|
134
|
+
- Sort, in descending order, the discovered components
|
|
135
|
+
<component-K/> (K=1-N) by their `rank` field and trim the result
|
|
136
|
+
list to just a maximum of 12 total components.
|
|
137
|
+
|
|
138
|
+
- For each component <component-K/> (K=1-N), research and then
|
|
139
|
+
decide which *one* of *USP* (Unique Selling Point -- what makes
|
|
140
|
+
it unique), *Crux* (what you should notice), or *Gotcha* (what
|
|
141
|
+
you should not stumble over) is its single most distinguishing
|
|
142
|
+
perspective, and remember this as an <info-K/> (K=1-N) formatted
|
|
143
|
+
like `<type/>: <hint/>` where <type/> is one of `USP`, `Crux`,
|
|
144
|
+
or `Gotcha` and <hint/> is a 1-6 word hint. Do not output
|
|
145
|
+
anything.
|
|
143
146
|
</step>
|
|
144
147
|
|
|
145
148
|
4. <step id="STEP 4: Report Components">
|
|
146
|
-
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
149
|
+
- Display the determined, individual components as a Markdown
|
|
150
|
+
*table* with just the following <template/> and do not output
|
|
151
|
+
anything else:
|
|
152
|
+
|
|
153
|
+
<template>
|
|
154
|
+
🔵 **COMPONENT HINTS**:
|
|
155
|
+
|
|
156
|
+
| ⚑ *Component* | ▣ *Package* | ⚖ *Hint* |
|
|
157
|
+
| :----------------- | :------------- | :-------- |
|
|
158
|
+
| **<component-1/>** | `<package-1/>` | <info-1/> |
|
|
159
|
+
[...]
|
|
160
|
+
| **<component-N/>** | `<package-N/>` | <info-N/> |
|
|
161
|
+
</template>
|
|
150
162
|
|
|
151
163
|
- Display the discovered components as a Markdown *table*
|
|
152
164
|
with just the following <template/>:
|
|
153
165
|
|
|
154
166
|
<template>
|
|
155
|
-
🔵 **
|
|
167
|
+
🔵 **COMPONENT RANKING**:
|
|
156
168
|
|
|
157
|
-
| *Component*
|
|
158
|
-
|
|
|
159
|
-
| **<
|
|
169
|
+
| ⚑ *Component* | ▣ *Package* | ❖ *Version* | ↓ *Downloads* | ⎈ *Stars* | ⏲ *Updated* | ☆ *Created* |
|
|
170
|
+
| :----------------- | :------------- | -----------: | -----------------: | -------------: | :--------------- | :----------- |
|
|
171
|
+
| **<component-1/>** | `<package-1/>` | <version-1/> | **<downloads-1/>** | **<stars-1/>** | **<updated-1/>** | <created-1/> |
|
|
160
172
|
[...]
|
|
161
|
-
| **<
|
|
173
|
+
| **<component-N/>** | `<package-N/>` | <version-N/> | **<downloads-N/>** | **<stars-N/>** | **<updated-N/>** | <created-N/> |
|
|
162
174
|
</template>
|
|
163
175
|
</step>
|
|
164
176
|
</flow>
|
|
@@ -12,7 +12,6 @@ allowed-tools:
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
14
|
@${CLAUDE_SKILL_DIR}/../../meta/ase-control.md
|
|
15
|
-
@${CLAUDE_SKILL_DIR}/../../meta/ase-persona.md
|
|
16
15
|
@${CLAUDE_SKILL_DIR}/../../meta/ase-skill.md
|
|
17
16
|
@${CLAUDE_SKILL_DIR}/../../meta/ase-dialog.md
|
|
18
17
|
@${CLAUDE_SKILL_DIR}/../../meta/ase-getopt.md
|
|
@@ -12,7 +12,6 @@ allowed-tools:
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
14
|
@${CLAUDE_SKILL_DIR}/../../meta/ase-control.md
|
|
15
|
-
@${CLAUDE_SKILL_DIR}/../../meta/ase-persona.md
|
|
16
15
|
@${CLAUDE_SKILL_DIR}/../../meta/ase-skill.md
|
|
17
16
|
@${CLAUDE_SKILL_DIR}/../../meta/ase-dialog.md
|
|
18
17
|
@${CLAUDE_SKILL_DIR}/../../meta/ase-getopt.md
|
|
@@ -12,7 +12,6 @@ allowed-tools:
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
14
|
@${CLAUDE_SKILL_DIR}/../../meta/ase-control.md
|
|
15
|
-
@${CLAUDE_SKILL_DIR}/../../meta/ase-persona.md
|
|
16
15
|
@${CLAUDE_SKILL_DIR}/../../meta/ase-skill.md
|
|
17
16
|
@${CLAUDE_SKILL_DIR}/../../meta/ase-dialog.md
|
|
18
17
|
@${CLAUDE_SKILL_DIR}/../../meta/ase-getopt.md
|
|
@@ -12,7 +12,6 @@ effort: high
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
14
|
@${CLAUDE_SKILL_DIR}/../../meta/ase-control.md
|
|
15
|
-
@${CLAUDE_SKILL_DIR}/../../meta/ase-persona.md
|
|
16
15
|
@${CLAUDE_SKILL_DIR}/../../meta/ase-skill.md
|
|
17
16
|
|
|
18
17
|
Evaluate Alternatives
|
|
@@ -153,13 +152,16 @@ multi-*criteria* decision matrix.
|
|
|
153
152
|
from { -2, -1, 0, +1, +2 } (from worst, over neutral, to best). Do
|
|
154
153
|
not output anything.
|
|
155
154
|
|
|
156
|
-
- Then,
|
|
157
|
-
|
|
158
|
-
<weight-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
155
|
+
- Then, calculate the ratings <rating-K/> (K=1-N) for all
|
|
156
|
+
alternatives in a single call by invoking the `decision_matrix(matrix:
|
|
157
|
+
[ [ <weight-1/>, <eval-1-1/>, ..., <eval-1-N/> ], ..., [ <weight-M/>,
|
|
158
|
+
<eval-M-1/>, ..., <eval-M-N/> ] ])` tool of the `ase` MCP service.
|
|
159
|
+
The tool returns an array of N numerical values, where the K-th
|
|
160
|
+
entry is the product-sum of all weights <weight-L/> (L=1-M) and
|
|
161
|
+
the evaluation <eval-K-L/> (L=1-M) for alternative <alternative-K/>.
|
|
162
|
+
Retain the *raw, unrounded* <rating-K/> for use in STEP 5, but
|
|
163
|
+
round <rating-K/> to 2 decimal places *for display only* when
|
|
164
|
+
emitting it in the table below. Do not output anything.
|
|
163
165
|
|
|
164
166
|
- Output the resulting *Weighted Decision Matrix* as a Markdown
|
|
165
167
|
*table* with just the following <template/> and do not output
|
|
@@ -13,7 +13,6 @@ effort: high
|
|
|
13
13
|
---
|
|
14
14
|
|
|
15
15
|
@${CLAUDE_SKILL_DIR}/../../meta/ase-control.md
|
|
16
|
-
@${CLAUDE_SKILL_DIR}/../../meta/ase-persona.md
|
|
17
16
|
@${CLAUDE_SKILL_DIR}/../../meta/ase-skill.md
|
|
18
17
|
@${CLAUDE_SKILL_DIR}/../../meta/ase-dialog.md
|
|
19
18
|
@${CLAUDE_SKILL_DIR}/../../meta/ase-getopt.md
|
|
@@ -11,7 +11,6 @@ effort: xhigh
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
@${CLAUDE_SKILL_DIR}/../../meta/ase-control.md
|
|
14
|
-
@${CLAUDE_SKILL_DIR}/../../meta/ase-persona.md
|
|
15
14
|
@${CLAUDE_SKILL_DIR}/../../meta/ase-skill.md
|
|
16
15
|
@${CLAUDE_SKILL_DIR}/../../meta/ase-dialog.md
|
|
17
16
|
@${CLAUDE_SKILL_DIR}/../../meta/ase-getopt.md
|
|
@@ -11,7 +11,6 @@ effort: xhigh
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
@${CLAUDE_SKILL_DIR}/../../meta/ase-control.md
|
|
14
|
-
@${CLAUDE_SKILL_DIR}/../../meta/ase-persona.md
|
|
15
14
|
@${CLAUDE_SKILL_DIR}/../../meta/ase-skill.md
|
|
16
15
|
@${CLAUDE_SKILL_DIR}/../../meta/ase-dialog.md
|
|
17
16
|
@${CLAUDE_SKILL_DIR}/../../meta/ase-getopt.md
|
|
@@ -11,7 +11,6 @@ effort: xhigh
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
@${CLAUDE_SKILL_DIR}/../../meta/ase-control.md
|
|
14
|
-
@${CLAUDE_SKILL_DIR}/../../meta/ase-persona.md
|
|
15
14
|
@${CLAUDE_SKILL_DIR}/../../meta/ase-skill.md
|
|
16
15
|
@${CLAUDE_SKILL_DIR}/../../meta/ase-dialog.md
|
|
17
16
|
@${CLAUDE_SKILL_DIR}/../../meta/ase-getopt.md
|