@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.
Files changed (33) hide show
  1. package/dst/ase-config.js +2 -0
  2. package/dst/ase-decision.js +67 -0
  3. package/dst/ase-kv.js +105 -6
  4. package/dst/ase-service.js +25 -26
  5. package/dst/ase-skills.js +224 -0
  6. package/package.json +6 -4
  7. package/plugin/.claude-plugin/plugin.json +1 -1
  8. package/plugin/.github/plugin/plugin.json +1 -1
  9. package/plugin/package.json +1 -1
  10. package/plugin/skills/ase-arch-analyze/SKILL.md +0 -1
  11. package/plugin/skills/ase-arch-discover/SKILL.md +44 -32
  12. package/plugin/skills/ase-code-analyze/SKILL.md +54 -56
  13. package/plugin/skills/ase-code-craft/SKILL.md +58 -21
  14. package/plugin/skills/ase-code-explain/SKILL.md +0 -1
  15. package/plugin/skills/ase-code-insight/SKILL.md +0 -1
  16. package/plugin/skills/ase-code-lint/SKILL.md +0 -1
  17. package/plugin/skills/ase-code-refactor/SKILL.md +46 -23
  18. package/plugin/skills/ase-code-resolve/SKILL.md +46 -34
  19. package/plugin/skills/ase-meta-changes/SKILL.md +0 -1
  20. package/plugin/skills/ase-meta-chat/SKILL.md +0 -1
  21. package/plugin/skills/ase-meta-commit/SKILL.md +0 -1
  22. package/plugin/skills/ase-meta-evaluate/SKILL.md +10 -8
  23. package/plugin/skills/ase-meta-quorum/SKILL.md +0 -1
  24. package/plugin/skills/ase-meta-search/SKILL.md +0 -1
  25. package/plugin/skills/ase-meta-why/SKILL.md +0 -1
  26. package/plugin/skills/ase-task-delete/SKILL.md +0 -1
  27. package/plugin/skills/ase-task-edit/SKILL.md +0 -1
  28. package/plugin/skills/ase-task-id/SKILL.md +0 -1
  29. package/plugin/skills/ase-task-implement/SKILL.md +0 -1
  30. package/plugin/skills/ase-task-list/SKILL.md +0 -1
  31. package/plugin/skills/ase-task-preflight/SKILL.md +0 -1
  32. package/plugin/skills/ase-task-reboot/SKILL.md +0 -1
  33. 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 string, up to 1024 characters)")
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 string, up to 1024 characters)"),
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 string, up to 1024 characters)")
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
  }
@@ -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 axios from "axios";
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 Axios transport errors */
34
+ /* distinguish ECONNREFUSED from other ofetch transport errors */
34
35
  export const isConnRefused = (err) => {
35
36
  const e = err;
36
- return e?.code === "ECONNREFUSED" || e?.cause?.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 axios.request({
44
+ const r = await ofetch.raw(`http://${SERVICE_HOST}:${port}/`, {
42
45
  method: "OPTIONS",
43
- url: `http://${SERVICE_HOST}:${port}/`,
44
- timeout: 2000,
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.data;
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 axios.request({
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
- data: { command: "status" },
474
- timeout: 2000,
475
- validateStatus: () => true
475
+ body: { command: "status" },
476
+ signal: AbortSignal.timeout(2000),
477
+ ignoreResponseError: true
476
478
  });
477
- const d = r.data;
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 axios.request({
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
- data: { command: cmd },
511
- timeout: 5000,
512
- validateStatus: () => true,
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.data === "string" ? r.data : JSON.stringify(r.data);
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 axios.request({
539
+ const r = await ofetch.raw(`http://${HOST}:${ctx.port}/stop`, {
540
540
  method: "GET",
541
- url: `http://${HOST}:${ctx.port}/stop`,
542
- timeout: 5000,
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.40",
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",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ase",
3
- "version": "0.0.40",
3
+ "version": "0.0.42",
4
4
  "description": "Agentic Software Engineering (ASE)",
5
5
  "keywords": [ "agentic", "software", "engineering" ],
6
6
  "homepage": "https://ase.tools",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ase",
3
- "version": "0.0.40",
3
+ "version": "0.0.42",
4
4
  "description": "Agentic Software Engineering (ASE)",
5
5
  "keywords": [ "agentic", "software", "engineering" ],
6
6
  "homepage": "https://ase.tools",
@@ -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.40",
9
+ "version": "0.0.42",
10
10
  "license": "GPL-3.0-only",
11
11
  "author": {
12
12
  "name": "Dr. Ralf S. Engelschall",
@@ -100,7 +100,6 @@ allowed-tools:
100
100
  ---
101
101
 
102
102
  @${CLAUDE_SKILL_DIR}/../../meta/ase-control.md
103
- @${CLAUDE_SKILL_DIR}/../../meta/ase-persona.md
104
103
  @${CLAUDE_SKILL_DIR}/../../meta/ase-skill.md
105
104
 
106
105
  Review Software Architecture