@rse/ase 0.0.46 → 0.0.49
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 +103 -6
- package/dst/ase-diagram.js +3 -3
- package/dst/ase-getopt.js +8 -4
- package/dst/ase-hook.js +122 -23
- package/dst/ase-mcp.js +26 -20
- package/dst/ase-service.js +10 -3
- package/dst/ase-skills.js +182 -42
- package/dst/ase-statusline.js +17 -21
- package/dst/ase-task.js +60 -1
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.github/plugin/plugin.json +1 -1
- package/plugin/agents/ase-code-lint.md +370 -0
- package/plugin/agents/ase-docs-proofread.md +100 -0
- package/plugin/meta/ase-constitution.md +7 -7
- package/plugin/meta/ase-control.md +1 -1
- package/plugin/meta/ase-dialog.md +2 -2
- package/plugin/meta/ase-persona.md +2 -2
- package/plugin/meta/ase-plan.md +2 -2
- package/plugin/meta/ase-skill.md +2 -2
- package/plugin/package.json +1 -1
- package/plugin/skills/ase-arch-analyze/SKILL.md +4 -4
- package/plugin/skills/ase-arch-discover/SKILL.md +14 -3
- package/plugin/skills/ase-code-analyze/SKILL.md +2 -2
- package/plugin/skills/ase-code-explain/SKILL.md +1 -1
- package/plugin/skills/ase-code-lint/SKILL.md +179 -298
- package/plugin/skills/ase-code-resolve/SKILL.md +1 -1
- package/plugin/skills/ase-docs-proofread/SKILL.md +151 -51
- package/plugin/skills/ase-meta-changes/SKILL.md +4 -4
- package/plugin/skills/ase-meta-diagram/SKILL.md +5 -5
- package/plugin/skills/ase-meta-evaluate/SKILL.md +2 -2
- package/plugin/skills/ase-meta-persona/SKILL.md +1 -1
- package/plugin/skills/ase-meta-quorum/SKILL.md +1 -1
- package/plugin/skills/ase-task-rename/SKILL.md +92 -0
package/dst/ase-skills.js
CHANGED
|
@@ -75,50 +75,184 @@ export class Skills {
|
|
|
75
75
|
return "N.A.";
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
|
+
/* fetch Maven Central metadata for a single coordinate `groupId:artifactId`:
|
|
79
|
+
the Solr search endpoint provides the latest version + a `timestamp` (ms
|
|
80
|
+
epoch) of that release; a second `core=gav` query yields the earliest
|
|
81
|
+
known release, which we treat as the "created" date. The latest POM is
|
|
82
|
+
then downloaded directly from the repo to extract the SCM/project URL. */
|
|
83
|
+
static async fetchMavenInfo(coord) {
|
|
84
|
+
const [groupId, artifactId] = coord.split(":", 2);
|
|
85
|
+
if (groupId === undefined || artifactId === undefined || groupId === "" || artifactId === "")
|
|
86
|
+
return { version: "", created: "", updated: "", repository: "" };
|
|
87
|
+
try {
|
|
88
|
+
const latest = await Skills.httpLimit(() => ofetch("https://search.maven.org/solrsearch/select" +
|
|
89
|
+
`?q=g:%22${encodeURIComponent(groupId)}%22+AND+a:%22${encodeURIComponent(artifactId)}%22` +
|
|
90
|
+
"&rows=1&wt=json", { timeout: Skills.HTTP_TIMEOUT_MS, ignoreResponseError: true }));
|
|
91
|
+
const doc = latest?.response?.docs?.[0];
|
|
92
|
+
const version = doc?.latestVersion ?? doc?.v ?? "";
|
|
93
|
+
const updated = typeof doc?.timestamp === "number" ? new Date(doc.timestamp).toISOString() : "";
|
|
94
|
+
/* Maven Central caps `rows` at 20 and ignores client `sort`,
|
|
95
|
+
so paginate via `start=` to walk all versions, accumulate the
|
|
96
|
+
minimum timestamp client-side */
|
|
97
|
+
const baseUrl = "https://search.maven.org/solrsearch/select" +
|
|
98
|
+
`?q=g:%22${encodeURIComponent(groupId)}%22+AND+a:%22${encodeURIComponent(artifactId)}%22` +
|
|
99
|
+
"&core=gav&rows=20&wt=json";
|
|
100
|
+
let firstTs;
|
|
101
|
+
let start = 0;
|
|
102
|
+
for (let page = 0; page < 50; page++) {
|
|
103
|
+
const chunk = await Skills.httpLimit(() => ofetch(`${baseUrl}&start=${start}`, { timeout: Skills.HTTP_TIMEOUT_MS, ignoreResponseError: true }));
|
|
104
|
+
const docs = chunk?.response?.docs ?? [];
|
|
105
|
+
for (const d of docs) {
|
|
106
|
+
if (typeof d.timestamp === "number" && (firstTs === undefined || d.timestamp < firstTs))
|
|
107
|
+
firstTs = d.timestamp;
|
|
108
|
+
}
|
|
109
|
+
start += docs.length;
|
|
110
|
+
const total = chunk?.response?.numFound ?? 0;
|
|
111
|
+
if (docs.length === 0 || start >= total)
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
const created = typeof firstTs === "number" ? new Date(firstTs).toISOString() : updated;
|
|
115
|
+
let repository = "";
|
|
116
|
+
if (version !== "") {
|
|
117
|
+
try {
|
|
118
|
+
const pom = await Skills.httpLimit(() => ofetch(`https://repo1.maven.org/maven2/${groupId.replace(/\./g, "/")}` +
|
|
119
|
+
`/${artifactId}/${version}/${artifactId}-${version}.pom`, { timeout: Skills.HTTP_TIMEOUT_MS, ignoreResponseError: true, responseType: "text" }));
|
|
120
|
+
if (typeof pom === "string") {
|
|
121
|
+
const scm = /<scm>[\s\S]*?<url>\s*([^<\s]+)\s*<\/url>[\s\S]*?<\/scm>/i.exec(pom);
|
|
122
|
+
if (scm !== null)
|
|
123
|
+
repository = scm[1];
|
|
124
|
+
else {
|
|
125
|
+
const url = /<project\b[\s\S]*?<url>\s*([^<\s]+)\s*<\/url>/i.exec(pom);
|
|
126
|
+
if (url !== null)
|
|
127
|
+
repository = url[1];
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
repository = "";
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return { version, created, updated, repository };
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return { version: "", created: "", updated: "", repository: "" };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/* fetch the "Used By" count from `mvnrepository.com` as a downloads proxy
|
|
142
|
+
for a Maven coordinate (Maven Central exposes no per-artifact download
|
|
143
|
+
counts). The label `Used By (NNK)` on the artifact landing page denotes
|
|
144
|
+
the number of other published artifacts depending on it. Note: the site
|
|
145
|
+
is fronted by *Cloudflare* and frequently serves a JS-challenge to
|
|
146
|
+
non-browser clients regardless of `User-Agent` (the block is driven by
|
|
147
|
+
TLS/HTTP2 fingerprinting, not headers). A realistic browser UA is
|
|
148
|
+
best-effort and degrades gracefully to `"N.A."` when blocked. */
|
|
149
|
+
static async fetchMavenDownloads(coord) {
|
|
150
|
+
const [groupId, artifactId] = coord.split(":", 2);
|
|
151
|
+
if (groupId === undefined || artifactId === undefined || groupId === "" || artifactId === "")
|
|
152
|
+
return "N.A.";
|
|
153
|
+
try {
|
|
154
|
+
const html = await Skills.httpLimit(() => ofetch(`https://mvnrepository.com/artifact/${encodeURIComponent(groupId)}/${encodeURIComponent(artifactId)}`, {
|
|
155
|
+
timeout: Skills.HTTP_TIMEOUT_MS,
|
|
156
|
+
ignoreResponseError: true,
|
|
157
|
+
responseType: "text",
|
|
158
|
+
headers: {
|
|
159
|
+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " +
|
|
160
|
+
"AppleWebKit/537.36 (KHTML, like Gecko) " +
|
|
161
|
+
"Chrome/131.0.0.0 Safari/537.36"
|
|
162
|
+
}
|
|
163
|
+
}));
|
|
164
|
+
if (typeof html !== "string")
|
|
165
|
+
return "N.A.";
|
|
166
|
+
const m = /Used\s+By\s*\(\s*([\d.,]+)\s*([KMB]?)\s*\)/i.exec(html);
|
|
167
|
+
if (m === null)
|
|
168
|
+
return "N.A.";
|
|
169
|
+
const base = parseFloat(m[1].replace(/,/g, ""));
|
|
170
|
+
const mult = m[2] === "K" ? 1_000 : m[2] === "M" ? 1_000_000 : m[2] === "B" ? 1_000_000_000 : 1;
|
|
171
|
+
const n = Math.round(base * mult);
|
|
172
|
+
return Number.isFinite(n) ? n : "N.A.";
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return "N.A.";
|
|
176
|
+
}
|
|
177
|
+
}
|
|
78
178
|
/* gather metadata for all given components with maximum parallelism,
|
|
79
179
|
dispatching on the technology stack:
|
|
80
180
|
- "JavaScript"/"TypeScript": NPM registry (pacote) + GitHub stars + npm-downloads
|
|
81
|
-
- "Java"/"Kotlin"
|
|
181
|
+
- "Java"/"Kotlin": Maven Central + GitHub stars + mvnrepository.com "Used By"
|
|
182
|
+
- "Unknown": not supported -- return empty result */
|
|
82
183
|
static async info(stack, components) {
|
|
83
|
-
|
|
84
|
-
|
|
184
|
+
if (stack === "JavaScript" || stack === "TypeScript") {
|
|
185
|
+
/* per package: kick off packument and downloads in parallel,
|
|
186
|
+
then stars as soon as the packument resolves; across packages
|
|
187
|
+
everything runs concurrently via Promise.all */
|
|
188
|
+
const results = await Promise.all(components.map(async (name) => {
|
|
189
|
+
const packumentPromise = Skills.fetchPackument(name);
|
|
190
|
+
const downloadsPromise = Skills.fetchDownloads(name);
|
|
191
|
+
const starsPromise = packumentPromise.then((p) => Skills.fetchStars(p.repository));
|
|
192
|
+
const [p, downloads, stars] = await Promise.all([
|
|
193
|
+
packumentPromise, downloadsPromise, starsPromise
|
|
194
|
+
]);
|
|
195
|
+
const created = p.time.created ?? "";
|
|
196
|
+
const updated = p.version !== "" ? (p.time[p.version] ?? "") : "";
|
|
197
|
+
const rank = Skills.computeRank(downloads, stars, created, updated);
|
|
198
|
+
return {
|
|
199
|
+
name,
|
|
200
|
+
version: p.version,
|
|
201
|
+
created,
|
|
202
|
+
updated,
|
|
203
|
+
repository: p.repository,
|
|
204
|
+
stars,
|
|
205
|
+
downloads,
|
|
206
|
+
rank
|
|
207
|
+
};
|
|
208
|
+
}));
|
|
209
|
+
/* sort by rank in descending order (best first) */
|
|
210
|
+
results.sort((a, b) => b.rank - a.rank);
|
|
211
|
+
return results;
|
|
212
|
+
}
|
|
213
|
+
else if (stack === "Java" || stack === "Kotlin") {
|
|
214
|
+
/* per coordinate: kick off Maven Central info and mvnrepository
|
|
215
|
+
downloads in parallel, then stars as soon as the POM-derived
|
|
216
|
+
repository is known; across coordinates everything runs
|
|
217
|
+
concurrently via Promise.all */
|
|
218
|
+
const results = await Promise.all(components.map(async (name) => {
|
|
219
|
+
const infoPromise = Skills.fetchMavenInfo(name);
|
|
220
|
+
const downloadsPromise = Skills.fetchMavenDownloads(name);
|
|
221
|
+
const starsPromise = infoPromise.then((i) => Skills.fetchStars(i.repository));
|
|
222
|
+
const [i, downloads, stars] = await Promise.all([
|
|
223
|
+
infoPromise, downloadsPromise, starsPromise
|
|
224
|
+
]);
|
|
225
|
+
const rank = Skills.computeRank(downloads, stars, i.created, i.updated);
|
|
226
|
+
return {
|
|
227
|
+
name,
|
|
228
|
+
version: i.version,
|
|
229
|
+
created: i.created,
|
|
230
|
+
updated: i.updated,
|
|
231
|
+
repository: i.repository,
|
|
232
|
+
stars,
|
|
233
|
+
downloads,
|
|
234
|
+
rank
|
|
235
|
+
};
|
|
236
|
+
}));
|
|
237
|
+
/* sort by rank in descending order (best first) */
|
|
238
|
+
results.sort((a, b) => b.rank - a.rank);
|
|
239
|
+
return results;
|
|
240
|
+
}
|
|
241
|
+
else
|
|
85
242
|
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
243
|
}
|
|
114
244
|
/* compute composite rank score from weighted metrics:
|
|
115
245
|
downloads x
|
|
116
246
|
stars x
|
|
117
247
|
([lifespan =] (updated - created)) x
|
|
118
|
-
([recentness =] exp(-(now - updated) / halfLife))
|
|
248
|
+
([recentness =] exp(-(now - updated) / halfLife))
|
|
249
|
+
`"N.A."` factors are treated as neutral `1` so that stacks for which
|
|
250
|
+
a particular metric is structurally unavailable (e.g. Maven Central
|
|
251
|
+
exposes no per-artifact download counts) can still be ranked by the
|
|
252
|
+
remaining metrics, instead of collapsing the entire product to zero. */
|
|
119
253
|
static computeRank(downloads, stars, created, updated) {
|
|
120
|
-
const d = typeof downloads === "number" ? downloads :
|
|
121
|
-
const s = typeof stars === "number" ? stars :
|
|
254
|
+
const d = typeof downloads === "number" ? downloads : 1;
|
|
255
|
+
const s = typeof stars === "number" ? stars : 1;
|
|
122
256
|
const cMs = created !== "" ? Date.parse(created) : NaN;
|
|
123
257
|
const uMs = updated !== "" ? Date.parse(updated) : NaN;
|
|
124
258
|
if (Number.isNaN(cMs) || Number.isNaN(uMs))
|
|
@@ -160,20 +294,26 @@ export class SkillsMCP {
|
|
|
160
294
|
register(mcp) {
|
|
161
295
|
mcp.registerTool("component_info", {
|
|
162
296
|
title: "ASE component info",
|
|
163
|
-
description: "Gather metadata for a list of
|
|
164
|
-
"
|
|
165
|
-
"
|
|
297
|
+
description: "Gather metadata for a list of packages with maximum parallelism, " +
|
|
298
|
+
"dispatching on the technology `stack`. For `JavaScript`/`TypeScript`, " +
|
|
299
|
+
"components are NPM package names: fetches the full registry packument via " +
|
|
300
|
+
"`pacote` (in-process, no `npm view` subprocess), the GitHub `stargazers_count` " +
|
|
166
301
|
"from `api.github.com` (if the repository points to GitHub), and the " +
|
|
167
|
-
"last-month downloads from `api.npmjs.org`.
|
|
168
|
-
"
|
|
169
|
-
"
|
|
170
|
-
"
|
|
171
|
-
"
|
|
302
|
+
"last-month downloads from `api.npmjs.org`. For `Java`/`Kotlin`, components " +
|
|
303
|
+
"are Maven coordinates of the form `groupId:artifactId`: fetches the latest " +
|
|
304
|
+
"version + earliest/latest release timestamps from the Maven Central Solr " +
|
|
305
|
+
"search endpoint, the SCM/project URL from the latest POM at `repo1.maven.org`, " +
|
|
306
|
+
"GitHub stars (if applicable), and the \"Used By\" count from `mvnrepository.com` " +
|
|
307
|
+
"as the `downloads` proxy. Returns a JSON `text` array of " +
|
|
308
|
+
"`{ name, version, created, updated, repository, stars, downloads, rank }` " +
|
|
309
|
+
"objects, sorted in descending order by `rank`. Failures of individual side " +
|
|
310
|
+
"calls are isolated and reported as `\"N.A.\"` or empty string so every entry " +
|
|
311
|
+
"has the full shape.",
|
|
172
312
|
inputSchema: {
|
|
173
313
|
stack: z.string()
|
|
174
314
|
.describe("Technology stack: \"JavaScript\", \"TypeScript\", \"Java\", \"Kotlin\", or \"Unknown\""),
|
|
175
315
|
components: z.array(z.string())
|
|
176
|
-
.describe("List of package names
|
|
316
|
+
.describe("List of package names (NPM) or Maven coordinates `groupId:artifactId` (Java/Kotlin)")
|
|
177
317
|
}
|
|
178
318
|
}, async (args) => {
|
|
179
319
|
try {
|
package/dst/ase-statusline.js
CHANGED
|
@@ -175,7 +175,7 @@ export default class StatuslineCommand {
|
|
|
175
175
|
/* parse and validate the --tool option */
|
|
176
176
|
parseTool(value) {
|
|
177
177
|
if (value !== "claude" && value !== "copilot")
|
|
178
|
-
throw new
|
|
178
|
+
throw new InvalidArgumentError(`invalid --tool value: "${value}" (expected "claude" or "copilot")`);
|
|
179
179
|
return value;
|
|
180
180
|
}
|
|
181
181
|
/* register commands */
|
|
@@ -357,7 +357,7 @@ export default class StatuslineCommand {
|
|
|
357
357
|
S: () => {
|
|
358
358
|
const pct5h = data.rate_limits?.five_hour?.used_percentage;
|
|
359
359
|
if (pct5h !== undefined)
|
|
360
|
-
emit(`${prefix("⏲", "session")}${c.bold(`${pct5h.toFixed(1)}%`)}`);
|
|
360
|
+
emit(`${prefix("⏲", "session-usage")}${c.bold(`${pct5h.toFixed(1)}%`)}`);
|
|
361
361
|
},
|
|
362
362
|
D: () => {
|
|
363
363
|
const until5h = data.rate_limits?.five_hour?.resets_at ?? "";
|
|
@@ -368,7 +368,7 @@ export default class StatuslineCommand {
|
|
|
368
368
|
W: () => {
|
|
369
369
|
const pctWk = data.rate_limits?.seven_day?.used_percentage;
|
|
370
370
|
if (pctWk !== undefined)
|
|
371
|
-
emit(`${prefix("⏲", "weekly")}${c.bold(`${pctWk.toFixed(1)}%`)}`);
|
|
371
|
+
emit(`${prefix("⏲", "weekly-usage")}${c.bold(`${pctWk.toFixed(1)}%`)}`);
|
|
372
372
|
},
|
|
373
373
|
Q: () => {
|
|
374
374
|
const untilWk = data.rate_limits?.seven_day?.resets_at ?? "";
|
|
@@ -390,11 +390,11 @@ export default class StatuslineCommand {
|
|
|
390
390
|
/* ==== VERSION CONTROL ==== */
|
|
391
391
|
a: () => {
|
|
392
392
|
const linesAdded = data.cost?.total_lines_added ?? 0;
|
|
393
|
-
emit(`${prefix("⊕", "added")}${c.bold(linesAdded)}`);
|
|
393
|
+
emit(`${prefix("⊕", "added")}${c.bold(String(linesAdded))}`);
|
|
394
394
|
},
|
|
395
395
|
r: () => {
|
|
396
396
|
const linesRemoved = data.cost?.total_lines_removed ?? 0;
|
|
397
|
-
emit(`${prefix("⊖", "removed")}${c.bold(linesRemoved)}`);
|
|
397
|
+
emit(`${prefix("⊖", "removed")}${c.bold(String(linesRemoved))}`);
|
|
398
398
|
},
|
|
399
399
|
b: () => {
|
|
400
400
|
const g = getGit();
|
|
@@ -435,6 +435,15 @@ export default class StatuslineCommand {
|
|
|
435
435
|
}
|
|
436
436
|
};
|
|
437
437
|
/* walk each template line and render */
|
|
438
|
+
const closeSpan = () => {
|
|
439
|
+
if (span !== null) {
|
|
440
|
+
const wrapped = span.color === "default" ?
|
|
441
|
+
span.buf :
|
|
442
|
+
(c[span.color])(span.buf);
|
|
443
|
+
span = null;
|
|
444
|
+
appendOutput(wrapped);
|
|
445
|
+
}
|
|
446
|
+
};
|
|
438
447
|
for (const line of tmpl) {
|
|
439
448
|
let i = 0;
|
|
440
449
|
while (i < line.length) {
|
|
@@ -443,15 +452,8 @@ export default class StatuslineCommand {
|
|
|
443
452
|
if (ch === "<") {
|
|
444
453
|
const m = line.slice(i).match(/^<(\/?)([a-z]+)>/);
|
|
445
454
|
if (m !== null && COLORS.has(m[2])) {
|
|
446
|
-
if (m[1] === "/")
|
|
447
|
-
|
|
448
|
-
const wrapped = span.color === "default" ?
|
|
449
|
-
span.buf :
|
|
450
|
-
(c[span.color])(span.buf);
|
|
451
|
-
span = null;
|
|
452
|
-
appendOutput(wrapped);
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
+
if (m[1] === "/")
|
|
456
|
+
closeSpan();
|
|
455
457
|
else if (span === null)
|
|
456
458
|
span = { color: m[2], buf: "" };
|
|
457
459
|
i += m[0].length;
|
|
@@ -468,13 +470,7 @@ export default class StatuslineCommand {
|
|
|
468
470
|
}
|
|
469
471
|
}
|
|
470
472
|
/* flush any unterminated span at end of line */
|
|
471
|
-
|
|
472
|
-
const wrapped = span.color === "default" ?
|
|
473
|
-
span.buf :
|
|
474
|
-
(c[span.color])(span.buf);
|
|
475
|
-
span = null;
|
|
476
|
-
appendOutput(wrapped);
|
|
477
|
-
}
|
|
473
|
+
closeSpan();
|
|
478
474
|
out += "\n";
|
|
479
475
|
col = 0;
|
|
480
476
|
}
|
package/dst/ase-task.js
CHANGED
|
@@ -69,6 +69,21 @@ export class Task {
|
|
|
69
69
|
fs.rmSync(path.dirname(file), { recursive: true, force: true });
|
|
70
70
|
return true;
|
|
71
71
|
}
|
|
72
|
+
/* rename a task by moving the entire task home directory
|
|
73
|
+
<project>/.ase/task/<oldId>/ to <project>/.ase/task/<newId>/;
|
|
74
|
+
returns true on success, false if the source task does not exist;
|
|
75
|
+
throws if the target id already exists */
|
|
76
|
+
static rename(oldId, newId) {
|
|
77
|
+
const oldDir = path.dirname(Task.path(oldId));
|
|
78
|
+
const newDir = path.dirname(Task.path(newId));
|
|
79
|
+
if (!fs.existsSync(oldDir))
|
|
80
|
+
return false;
|
|
81
|
+
if (fs.existsSync(newDir))
|
|
82
|
+
throw new Error(`task: target id "${newId}" already exists`);
|
|
83
|
+
fs.mkdirSync(path.dirname(newDir), { recursive: true });
|
|
84
|
+
fs.renameSync(oldDir, newDir);
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
72
87
|
/* list all persisted tasks in lexicographic id order; if verbose is true,
|
|
73
88
|
each entry's `mtime` is set to the `plan.md` modification time formatted
|
|
74
89
|
as "YYYY-MM-DD HH:MM", otherwise it is left undefined */
|
|
@@ -227,6 +242,20 @@ export default class TaskCommand {
|
|
|
227
242
|
this.log.write("info", `task: no task "${id}" to remove`);
|
|
228
243
|
process.exit(removed ? 0 : 1);
|
|
229
244
|
});
|
|
245
|
+
/* register CLI sub-command "ase task rename" */
|
|
246
|
+
task
|
|
247
|
+
.command("rename")
|
|
248
|
+
.description("Rename a task from <old> to <new>")
|
|
249
|
+
.argument("<old>", "Old task identifier")
|
|
250
|
+
.argument("<new>", "New task identifier")
|
|
251
|
+
.action((oldId, newId) => {
|
|
252
|
+
const renamed = Task.rename(oldId, newId);
|
|
253
|
+
if (renamed)
|
|
254
|
+
this.log.write("info", `task: renamed "${oldId}" to "${newId}"`);
|
|
255
|
+
else
|
|
256
|
+
this.log.write("info", `task: no task "${oldId}" to rename`);
|
|
257
|
+
process.exit(renamed ? 0 : 1);
|
|
258
|
+
});
|
|
230
259
|
/* register CLI sub-command "ase task purge" */
|
|
231
260
|
task
|
|
232
261
|
.command("purge")
|
|
@@ -289,7 +318,7 @@ export class TaskMCP {
|
|
|
289
318
|
const verbose = args.verbose ?? false;
|
|
290
319
|
const items = Task.list(verbose);
|
|
291
320
|
const tasks = verbose ?
|
|
292
|
-
items.map((item) => ({ id: item.id, mtime: item.mtime })) :
|
|
321
|
+
items.map((item) => ({ id: item.id, mtime: item.mtime ?? "" })) :
|
|
293
322
|
items.map((item) => ({ id: item.id }));
|
|
294
323
|
const result = { tasks };
|
|
295
324
|
return {
|
|
@@ -381,6 +410,36 @@ export class TaskMCP {
|
|
|
381
410
|
};
|
|
382
411
|
}
|
|
383
412
|
});
|
|
413
|
+
/* task rename */
|
|
414
|
+
mcp.registerTool("task_rename", {
|
|
415
|
+
title: "ASE task rename",
|
|
416
|
+
description: "Rename a previously persisted task from `old` to `new` by atomically moving the " +
|
|
417
|
+
"task home directory. Returns a status `text` indicating whether the rename succeeded. " +
|
|
418
|
+
"Fails with an error if the target id already exists.",
|
|
419
|
+
inputSchema: {
|
|
420
|
+
old: z.string()
|
|
421
|
+
.describe("old task identifier (allowed characters: A-Z, a-z, 0-9, '-')"),
|
|
422
|
+
new: z.string()
|
|
423
|
+
.describe("new task identifier (allowed characters: A-Z, a-z, 0-9, '-')")
|
|
424
|
+
}
|
|
425
|
+
}, async (args) => {
|
|
426
|
+
try {
|
|
427
|
+
const renamed = Task.rename(args.old, args.new);
|
|
428
|
+
const msg = renamed ?
|
|
429
|
+
`task_rename: OK: renamed task "${args.old}" to "${args.new}"` :
|
|
430
|
+
"WARNING: task not found";
|
|
431
|
+
return {
|
|
432
|
+
content: [{ type: "text", text: msg }]
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
catch (err) {
|
|
436
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
437
|
+
return {
|
|
438
|
+
isError: true,
|
|
439
|
+
content: [{ type: "text", text: `ERROR: ${message}` }]
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
});
|
|
384
443
|
/* task id get/set */
|
|
385
444
|
mcp.registerTool("task_id", {
|
|
386
445
|
title: "ASE task id get/set",
|
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.49",
|
|
10
10
|
"license": "GPL-3.0-only",
|
|
11
11
|
"author": {
|
|
12
12
|
"name": "Dr. Ralf S. Engelschall",
|