@rse/ase 0.0.40 → 0.0.42
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-kv.js +105 -6
- 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 +54 -56
- package/plugin/skills/ase-code-craft/SKILL.md +58 -21
- 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 +46 -23
- package/plugin/skills/ase-code-resolve/SKILL.md +46 -34
- 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-kv.js
CHANGED
|
@@ -17,6 +17,11 @@ export class KV {
|
|
|
17
17
|
static validateKey(key) {
|
|
18
18
|
if (typeof key !== "string" || key.length === 0)
|
|
19
19
|
throw new Error("kv: key must be a non-empty string");
|
|
20
|
+
if (key.trim().length === 0)
|
|
21
|
+
throw new Error("kv: key must not consist solely of whitespace");
|
|
22
|
+
/* eslint-disable-next-line no-control-regex */
|
|
23
|
+
if (/[\x00-\x1F\x7F]/.test(key))
|
|
24
|
+
throw new Error("kv: key must not contain control characters");
|
|
20
25
|
if (key.length > KV.KEY_MAX_LEN)
|
|
21
26
|
throw new Error(`kv: key must be no longer than ${KV.KEY_MAX_LEN} characters`);
|
|
22
27
|
}
|
|
@@ -33,7 +38,7 @@ export class KV {
|
|
|
33
38
|
/* set a value under the given key; overwrites any existing value */
|
|
34
39
|
static set(key, val) {
|
|
35
40
|
KV.validateKey(key);
|
|
36
|
-
KV.store.set(key, val);
|
|
41
|
+
KV.store.set(key, structuredClone(val));
|
|
37
42
|
}
|
|
38
43
|
/* delete a value by key; returns true if a value existed */
|
|
39
44
|
static delete(key) {
|
|
@@ -46,6 +51,16 @@ export class KV {
|
|
|
46
51
|
KV.store.clear();
|
|
47
52
|
return n;
|
|
48
53
|
}
|
|
54
|
+
/* snapshot the entire store into a fresh map (for transactional batch) */
|
|
55
|
+
static snapshot() {
|
|
56
|
+
return new Map(KV.store);
|
|
57
|
+
}
|
|
58
|
+
/* restore the store from a previously taken snapshot */
|
|
59
|
+
static restore(snap) {
|
|
60
|
+
KV.store.clear();
|
|
61
|
+
for (const [k, v] of snap)
|
|
62
|
+
KV.store.set(k, v);
|
|
63
|
+
}
|
|
49
64
|
}
|
|
50
65
|
/* MCP registration entry point for in-memory key/value tools */
|
|
51
66
|
export class KVMCP {
|
|
@@ -57,14 +72,14 @@ export class KVMCP {
|
|
|
57
72
|
"Returns the value as JSON-encoded `text`; returns an empty string if no value is stored.",
|
|
58
73
|
inputSchema: {
|
|
59
74
|
key: z.string()
|
|
60
|
-
.describe("key identifier (non-empty
|
|
75
|
+
.describe("key identifier (non-empty, no whitespace-only, no control characters, up to 1024 characters)")
|
|
61
76
|
}
|
|
62
77
|
}, async (args) => {
|
|
63
78
|
try {
|
|
64
79
|
if (!KV.has(args.key))
|
|
65
80
|
return { content: [{ type: "text", text: "" }] };
|
|
66
81
|
const val = KV.get(args.key);
|
|
67
|
-
const text = JSON.stringify(val);
|
|
82
|
+
const text = val === undefined ? "" : JSON.stringify(val);
|
|
68
83
|
return { content: [{ type: "text", text }] };
|
|
69
84
|
}
|
|
70
85
|
catch (err) {
|
|
@@ -80,8 +95,8 @@ export class KVMCP {
|
|
|
80
95
|
"The value can be any JSON-compatible type (string, number, boolean, null, array, object).",
|
|
81
96
|
inputSchema: {
|
|
82
97
|
key: z.string()
|
|
83
|
-
.describe("key identifier (non-empty
|
|
84
|
-
val: z.any()
|
|
98
|
+
.describe("key identifier (non-empty, no whitespace-only, no control characters, up to 1024 characters)"),
|
|
99
|
+
val: z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(z.any()), z.record(z.string(), z.any())])
|
|
85
100
|
.describe("arbitrary JSON-compatible value to store under `key`")
|
|
86
101
|
}
|
|
87
102
|
}, async (args) => {
|
|
@@ -117,7 +132,7 @@ export class KVMCP {
|
|
|
117
132
|
"Returns a status `text` indicating whether a value existed and was removed.",
|
|
118
133
|
inputSchema: {
|
|
119
134
|
key: z.string()
|
|
120
|
-
.describe("key identifier (non-empty
|
|
135
|
+
.describe("key identifier (non-empty, no whitespace-only, no control characters, up to 1024 characters)")
|
|
121
136
|
}
|
|
122
137
|
}, async (args) => {
|
|
123
138
|
try {
|
|
@@ -132,5 +147,89 @@ export class KVMCP {
|
|
|
132
147
|
return { isError: true, content: [{ type: "text", text: `kv_delete: ERROR: ${message}` }] };
|
|
133
148
|
}
|
|
134
149
|
});
|
|
150
|
+
/* key/value batch */
|
|
151
|
+
mcp.registerTool("kv_batch", {
|
|
152
|
+
title: "ASE key/value batch",
|
|
153
|
+
description: "Execute an array of in-memory key/value `commands` in a single MCP call. " +
|
|
154
|
+
"Each entry is an object `{ command: \"clear\"|\"set\"|\"get\"|\"delete\", key?, val? }` " +
|
|
155
|
+
"and is dispatched to the corresponding single-op tool. " +
|
|
156
|
+
"If `transactional` is true, the store is snapshotted up-front and rolled back on the " +
|
|
157
|
+
"first per-command error (remaining commands are skipped); otherwise per-command errors " +
|
|
158
|
+
"are recorded and execution continues. " +
|
|
159
|
+
"Returns a single `text` payload containing a JSON array of per-command result strings " +
|
|
160
|
+
"in the same format emitted by `kv_clear`/`kv_set`/`kv_get`/`kv_delete`. " +
|
|
161
|
+
"On transactional rollback, prior per-command result strings are rewritten to " +
|
|
162
|
+
"`kv_<cmd>: ROLLED-BACK` to truthfully reflect the post-rollback state, and the " +
|
|
163
|
+
"final entry remains `kv_batch: ERROR: <message>`.",
|
|
164
|
+
inputSchema: {
|
|
165
|
+
commands: z.array(z.object({
|
|
166
|
+
command: z.enum(["clear", "set", "get", "delete"])
|
|
167
|
+
.describe("the KV sub-command to execute"),
|
|
168
|
+
key: z.string().optional()
|
|
169
|
+
.describe("key identifier (required for `set`/`get`/`delete`)"),
|
|
170
|
+
val: z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(z.any()), z.record(z.string(), z.any())]).optional()
|
|
171
|
+
.describe("value to store (required for `set`)")
|
|
172
|
+
}))
|
|
173
|
+
.describe("ordered list of KV commands to execute"),
|
|
174
|
+
transactional: z.boolean().optional()
|
|
175
|
+
.describe("if true, snapshot the store and roll back on first error")
|
|
176
|
+
}
|
|
177
|
+
}, async (args) => {
|
|
178
|
+
const results = [];
|
|
179
|
+
const cmdNames = [];
|
|
180
|
+
const tx = args.transactional === true;
|
|
181
|
+
const snapshot = tx ? KV.snapshot() : null;
|
|
182
|
+
for (const c of args.commands) {
|
|
183
|
+
try {
|
|
184
|
+
if (c.command === "clear") {
|
|
185
|
+
const n = KV.clear();
|
|
186
|
+
results.push(`kv_clear: OK: removed ${n} key(s)`);
|
|
187
|
+
cmdNames.push("clear");
|
|
188
|
+
}
|
|
189
|
+
else if (c.command === "set") {
|
|
190
|
+
if (c.key === undefined)
|
|
191
|
+
throw new Error("kv_set: missing `key`");
|
|
192
|
+
if (c.val === undefined)
|
|
193
|
+
throw new Error("kv_set: missing `val`");
|
|
194
|
+
KV.set(c.key, c.val);
|
|
195
|
+
results.push(`kv_set: OK: stored key "${c.key}"`);
|
|
196
|
+
cmdNames.push("set");
|
|
197
|
+
}
|
|
198
|
+
else if (c.command === "get") {
|
|
199
|
+
if (c.key === undefined)
|
|
200
|
+
throw new Error("kv_get: missing `key`");
|
|
201
|
+
if (!KV.has(c.key))
|
|
202
|
+
results.push("");
|
|
203
|
+
else {
|
|
204
|
+
const val = KV.get(c.key);
|
|
205
|
+
results.push(val === undefined ? "" : JSON.stringify(val));
|
|
206
|
+
}
|
|
207
|
+
cmdNames.push("get");
|
|
208
|
+
}
|
|
209
|
+
else if (c.command === "delete") {
|
|
210
|
+
if (c.key === undefined)
|
|
211
|
+
throw new Error("kv_delete: missing `key`");
|
|
212
|
+
const removed = KV.delete(c.key);
|
|
213
|
+
results.push(removed ?
|
|
214
|
+
`kv_delete: OK: removed key "${c.key}"` :
|
|
215
|
+
`kv_delete: WARNING: no key "${c.key}" to remove`);
|
|
216
|
+
cmdNames.push("delete");
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch (err) {
|
|
220
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
221
|
+
if (tx) {
|
|
222
|
+
if (snapshot !== null)
|
|
223
|
+
KV.restore(snapshot);
|
|
224
|
+
for (let i = 0; i < results.length; i++)
|
|
225
|
+
results[i] = `kv_${cmdNames[i]}: ROLLED-BACK`;
|
|
226
|
+
results.push(`kv_batch: ERROR: ${message}`);
|
|
227
|
+
return { isError: true, content: [{ type: "text", text: JSON.stringify(results) }] };
|
|
228
|
+
}
|
|
229
|
+
results.push(`kv_${c.command}: ERROR: ${message}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return { content: [{ type: "text", text: JSON.stringify(results) }] };
|
|
233
|
+
});
|
|
135
234
|
}
|
|
136
235
|
}
|
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.42",
|
|
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.42",
|
|
10
10
|
"license": "GPL-3.0-only",
|
|
11
11
|
"author": {
|
|
12
12
|
"name": "Dr. Ralf S. Engelschall",
|