@rse/ase 0.0.17 → 0.0.19
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 +15 -97
- package/dst/ase-hook.js +3 -3
- package/dst/ase-plan.js +143 -0
- package/dst/ase-service.js +195 -2
- package/dst/ase-setup.js +16 -3
- package/dst/ase-statusline.js +134 -0
- package/dst/ase-task.js +195 -0
- package/dst/ase.js +4 -0
- package/package.json +1 -1
package/dst/ase-config.js
CHANGED
|
@@ -13,100 +13,45 @@ import * as v from "valibot";
|
|
|
13
13
|
import Table from "cli-table3";
|
|
14
14
|
/* classification taxonomy */
|
|
15
15
|
export const projectClassification = {
|
|
16
|
-
|
|
17
|
-
ambition: ["artist", "craftsman", "engineer"],
|
|
18
|
-
boxing: ["white", "grey", "black"],
|
|
19
|
-
size: ["small", "medium", "large"],
|
|
20
|
-
structure: ["bare", "library", "framework"]
|
|
21
|
-
},
|
|
22
|
-
process: {
|
|
23
|
-
actors: ["person", "team", "crew"],
|
|
24
|
-
drive: ["spec", "code", "test"]
|
|
25
|
-
},
|
|
26
|
-
result: {
|
|
27
|
-
target: ["prototype", "mvp", "product"]
|
|
28
|
-
}
|
|
16
|
+
boxing: ["white", "grey", "black"]
|
|
29
17
|
};
|
|
30
18
|
/* agent classification taxonomy */
|
|
31
19
|
export const agentClassification = {
|
|
32
|
-
persona:
|
|
33
|
-
style: ["writer", "engineer", "telegrapher", "caveman"],
|
|
34
|
-
creativity: ["none", "lite", "full"]
|
|
35
|
-
},
|
|
36
|
-
process: {
|
|
37
|
-
autonomy: ["assistant", "hotl", "agent"]
|
|
38
|
-
}
|
|
20
|
+
persona: ["writer", "engineer", "telegrapher", "caveman"]
|
|
39
21
|
};
|
|
40
22
|
/* classification presets */
|
|
41
23
|
export const projectClassificationPresets = {
|
|
42
24
|
vibe: {
|
|
43
25
|
"project.id": "example",
|
|
44
26
|
"project.name": "Example Project",
|
|
45
|
-
"project.
|
|
46
|
-
"
|
|
47
|
-
"project.source.size": "small",
|
|
48
|
-
"project.source.structure": "bare",
|
|
49
|
-
"project.process.actors": "person",
|
|
50
|
-
"project.process.drive": "spec",
|
|
51
|
-
"project.result.target": "prototype",
|
|
52
|
-
"agent.persona.style": "writer",
|
|
53
|
-
"agent.persona.creativity": "full",
|
|
54
|
-
"agent.process.autonomy": "agent"
|
|
27
|
+
"project.boxing": "black",
|
|
28
|
+
"agent.persona": "writer"
|
|
55
29
|
},
|
|
56
30
|
pro: {
|
|
57
31
|
"project.id": "example",
|
|
58
32
|
"project.name": "Example Project",
|
|
59
|
-
"project.
|
|
60
|
-
"
|
|
61
|
-
"project.source.size": "medium",
|
|
62
|
-
"project.source.structure": "framework",
|
|
63
|
-
"project.process.actors": "person",
|
|
64
|
-
"project.process.drive": "code",
|
|
65
|
-
"project.result.target": "product",
|
|
66
|
-
"agent.persona.style": "engineer",
|
|
67
|
-
"agent.persona.creativity": "none",
|
|
68
|
-
"agent.process.autonomy": "assistant"
|
|
33
|
+
"project.boxing": "white",
|
|
34
|
+
"agent.persona": "engineer"
|
|
69
35
|
},
|
|
70
36
|
default: {
|
|
71
37
|
"project.id": "example",
|
|
72
38
|
"project.name": "Example Project",
|
|
73
|
-
"project.
|
|
74
|
-
"
|
|
75
|
-
"
|
|
76
|
-
"project.source.structure": "framework",
|
|
77
|
-
"project.process.actors": "person",
|
|
78
|
-
"project.process.drive": "code",
|
|
79
|
-
"project.result.target": "product",
|
|
80
|
-
"project.artifact.build": "{etc/**,README.md,AGENTS.md,LICENSE.txt,package.json}",
|
|
81
|
-
"project.artifact.code": "src/**/*",
|
|
82
|
-
"project.artifact.docs": "doc/user/**/*.md",
|
|
83
|
-
"project.artifact.spec": "doc/spec/**/*.md",
|
|
84
|
-
"project.artifact.arch": "doc/arch/**/*.md",
|
|
85
|
-
"agent.persona.style": "engineer",
|
|
86
|
-
"agent.persona.creativity": "none",
|
|
87
|
-
"agent.process.autonomy": "assistant",
|
|
88
|
-
"task.id": "default"
|
|
39
|
+
"project.boxing": "white",
|
|
40
|
+
"agent.persona": "engineer",
|
|
41
|
+
"agent.task": "default"
|
|
89
42
|
},
|
|
90
43
|
industry: {
|
|
91
44
|
"project.id": "example",
|
|
92
45
|
"project.name": "Example Project",
|
|
93
|
-
"project.
|
|
94
|
-
"
|
|
95
|
-
"project.source.size": "large",
|
|
96
|
-
"project.source.structure": "framework",
|
|
97
|
-
"project.process.actors": "crew",
|
|
98
|
-
"project.process.drive": "code",
|
|
99
|
-
"project.result.target": "mvp",
|
|
100
|
-
"agent.persona.style": "engineer",
|
|
101
|
-
"agent.persona.creativity": "none",
|
|
102
|
-
"agent.process.autonomy": "hotl"
|
|
46
|
+
"project.boxing": "grey",
|
|
47
|
+
"agent.persona": "engineer"
|
|
103
48
|
}
|
|
104
49
|
};
|
|
105
50
|
/* hard-coded map: which scope kinds each variable may be SET on
|
|
106
51
|
(reads always cascade through the full chain, this restricts writes only);
|
|
107
52
|
keys absent from this map default to all non-"default" scope kinds */
|
|
108
53
|
export const configWritableScopes = {
|
|
109
|
-
"task
|
|
54
|
+
"agent.task": ["session"]
|
|
110
55
|
};
|
|
111
56
|
/* default set of scope kinds writable for any unrestricted key */
|
|
112
57
|
const configWritableScopesDefault = ["user", "project", "task", "session"];
|
|
@@ -181,38 +126,11 @@ export const configSchema = v.nullish(v.strictObject({
|
|
|
181
126
|
project: v.optional(v.strictObject({
|
|
182
127
|
id: v.optional(v.pipe(v.string(), v.minLength(1))),
|
|
183
128
|
name: v.optional(v.pipe(v.string(), v.minLength(1))),
|
|
184
|
-
|
|
185
|
-
ambition: v.optional(v.picklist(projectClassification.source.ambition)),
|
|
186
|
-
boxing: v.optional(v.picklist(projectClassification.source.boxing)),
|
|
187
|
-
size: v.optional(v.picklist(projectClassification.source.size)),
|
|
188
|
-
structure: v.optional(v.picklist(projectClassification.source.structure))
|
|
189
|
-
})),
|
|
190
|
-
process: v.optional(v.strictObject({
|
|
191
|
-
actors: v.optional(v.picklist(projectClassification.process.actors)),
|
|
192
|
-
drive: v.optional(v.picklist(projectClassification.process.drive))
|
|
193
|
-
})),
|
|
194
|
-
result: v.optional(v.strictObject({
|
|
195
|
-
target: v.optional(v.picklist(projectClassification.result.target))
|
|
196
|
-
})),
|
|
197
|
-
artifact: v.optional(v.strictObject({
|
|
198
|
-
build: v.optional(v.pipe(v.string(), v.minLength(1))),
|
|
199
|
-
code: v.optional(v.pipe(v.string(), v.minLength(1))),
|
|
200
|
-
docs: v.optional(v.pipe(v.string(), v.minLength(1))),
|
|
201
|
-
spec: v.optional(v.pipe(v.string(), v.minLength(1))),
|
|
202
|
-
arch: v.optional(v.pipe(v.string(), v.minLength(1)))
|
|
203
|
-
}))
|
|
129
|
+
boxing: v.optional(v.picklist(projectClassification.boxing))
|
|
204
130
|
})),
|
|
205
131
|
agent: v.optional(v.strictObject({
|
|
206
|
-
persona: v.optional(v.
|
|
207
|
-
|
|
208
|
-
creativity: v.optional(v.picklist(agentClassification.persona.creativity))
|
|
209
|
-
})),
|
|
210
|
-
process: v.optional(v.strictObject({
|
|
211
|
-
autonomy: v.optional(v.picklist(agentClassification.process.autonomy))
|
|
212
|
-
}))
|
|
213
|
-
})),
|
|
214
|
-
task: v.optional(v.strictObject({
|
|
215
|
-
id: v.optional(v.pipe(v.string(), v.minLength(1)))
|
|
132
|
+
persona: v.optional(v.picklist(agentClassification.persona)),
|
|
133
|
+
task: v.optional(v.pipe(v.string(), v.minLength(1)))
|
|
216
134
|
}))
|
|
217
135
|
}));
|
|
218
136
|
/* encapsulate read/write access to a stack of "<name>.yaml" configuration files,
|
package/dst/ase-hook.js
CHANGED
|
@@ -80,7 +80,7 @@ export default class HookCommand {
|
|
|
80
80
|
/* determine task id */
|
|
81
81
|
const taskId = process.env.ASE_TASK_ID ?? "default";
|
|
82
82
|
try {
|
|
83
|
-
cfg.set("task
|
|
83
|
+
cfg.set("agent.task", taskId);
|
|
84
84
|
cfg.write();
|
|
85
85
|
}
|
|
86
86
|
catch (_e) {
|
|
@@ -103,8 +103,8 @@ export default class HookCommand {
|
|
|
103
103
|
/* determine user id */
|
|
104
104
|
const userId = process.env.USER ?? process.env.LOGNAME ?? "unknown";
|
|
105
105
|
/* determine agent persona style */
|
|
106
|
-
let persona = "engineer";
|
|
107
|
-
const val = cfg.get("agent.persona
|
|
106
|
+
let persona = process.env.ASE_PERSONA_STYLE ?? "engineer";
|
|
107
|
+
const val = cfg.get("agent.persona");
|
|
108
108
|
if (typeof val === "string")
|
|
109
109
|
persona = val;
|
|
110
110
|
/* provide ASE information to Claude Code shell commands */
|
package/dst/ase-plan.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
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 path from "node:path";
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
/* validate the plan id to keep it safe as a filename component */
|
|
10
|
+
const validateId = (id) => {
|
|
11
|
+
if (typeof id !== "string" || id.length === 0)
|
|
12
|
+
throw new Error("plan: id must be a non-empty string");
|
|
13
|
+
if (!/^[A-Za-z0-9-]+$/.test(id))
|
|
14
|
+
throw new Error("plan: id must match [A-Za-z0-9-]+");
|
|
15
|
+
};
|
|
16
|
+
/* resolve the on-disk path for a given plan id */
|
|
17
|
+
const planPath = (id) => {
|
|
18
|
+
validateId(id);
|
|
19
|
+
return path.join(os.homedir(), ".ase", "plan", `${id}.md`);
|
|
20
|
+
};
|
|
21
|
+
/* load a plan; returns empty string if no plan exists */
|
|
22
|
+
export const planLoad = (id) => {
|
|
23
|
+
const file = planPath(id);
|
|
24
|
+
if (!fs.existsSync(file))
|
|
25
|
+
return "";
|
|
26
|
+
return fs.readFileSync(file, "utf8");
|
|
27
|
+
};
|
|
28
|
+
/* save a plan as UTF-8 text under the given id */
|
|
29
|
+
export const planSave = (id, text) => {
|
|
30
|
+
if (typeof text !== "string")
|
|
31
|
+
throw new Error("plan: text must be a string");
|
|
32
|
+
const file = planPath(id);
|
|
33
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
34
|
+
fs.writeFileSync(file, text, "utf8");
|
|
35
|
+
};
|
|
36
|
+
/* delete a plan by id; returns true if a plan existed and was removed */
|
|
37
|
+
export const planDelete = (id) => {
|
|
38
|
+
const file = planPath(id);
|
|
39
|
+
if (!fs.existsSync(file))
|
|
40
|
+
return false;
|
|
41
|
+
fs.rmSync(file);
|
|
42
|
+
return true;
|
|
43
|
+
};
|
|
44
|
+
/* purge plans whose modification time is older than the given cutoff in
|
|
45
|
+
milliseconds; returns the list of removed plan ids */
|
|
46
|
+
export const planPurge = (maxAgeMs) => {
|
|
47
|
+
const dir = path.join(os.homedir(), ".ase", "plan");
|
|
48
|
+
if (!fs.existsSync(dir))
|
|
49
|
+
return [];
|
|
50
|
+
const cutoff = Date.now() - maxAgeMs;
|
|
51
|
+
const removed = [];
|
|
52
|
+
for (const entry of fs.readdirSync(dir)) {
|
|
53
|
+
if (!entry.endsWith(".md"))
|
|
54
|
+
continue;
|
|
55
|
+
const file = path.join(dir, entry);
|
|
56
|
+
const st = fs.statSync(file);
|
|
57
|
+
if (!st.isFile())
|
|
58
|
+
continue;
|
|
59
|
+
if (st.mtimeMs < cutoff) {
|
|
60
|
+
fs.rmSync(file);
|
|
61
|
+
removed.push(entry.slice(0, -3));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return removed;
|
|
65
|
+
};
|
|
66
|
+
/* read all of stdin as a UTF-8 string */
|
|
67
|
+
const readStdin = () => {
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
const chunks = [];
|
|
70
|
+
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
71
|
+
process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
72
|
+
process.stdin.on("error", (err) => reject(err));
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
/* CLI command "ase plan" */
|
|
76
|
+
export default class PlanCommand {
|
|
77
|
+
log;
|
|
78
|
+
constructor(log) {
|
|
79
|
+
this.log = log;
|
|
80
|
+
}
|
|
81
|
+
/* register commands */
|
|
82
|
+
register(program) {
|
|
83
|
+
/* register CLI top-level command "ase plan" */
|
|
84
|
+
const plan = program
|
|
85
|
+
.command("plan")
|
|
86
|
+
.description("Manage persisted plans under ~/.ase/plan/<id>.md")
|
|
87
|
+
.action(() => {
|
|
88
|
+
plan.outputHelp();
|
|
89
|
+
process.exit(1);
|
|
90
|
+
});
|
|
91
|
+
/* register CLI sub-command "ase plan load" */
|
|
92
|
+
plan
|
|
93
|
+
.command("load")
|
|
94
|
+
.description("Load a plan by id and write it to stdout")
|
|
95
|
+
.argument("<id>", "Plan identifier")
|
|
96
|
+
.action((id) => {
|
|
97
|
+
const text = planLoad(id);
|
|
98
|
+
process.stdout.write(text);
|
|
99
|
+
process.exit(0);
|
|
100
|
+
});
|
|
101
|
+
/* register CLI sub-command "ase plan save" */
|
|
102
|
+
plan
|
|
103
|
+
.command("save")
|
|
104
|
+
.description("Save a plan by id, reading content from stdin")
|
|
105
|
+
.argument("<id>", "Plan identifier")
|
|
106
|
+
.action(async (id) => {
|
|
107
|
+
const text = await readStdin();
|
|
108
|
+
planSave(id, text);
|
|
109
|
+
this.log.write("info", `plan: saved "${id}"`);
|
|
110
|
+
process.exit(0);
|
|
111
|
+
});
|
|
112
|
+
/* register CLI sub-command "ase plan delete" */
|
|
113
|
+
plan
|
|
114
|
+
.command("delete")
|
|
115
|
+
.description("Delete a plan by id")
|
|
116
|
+
.argument("<id>", "Plan identifier")
|
|
117
|
+
.action((id) => {
|
|
118
|
+
const removed = planDelete(id);
|
|
119
|
+
if (removed)
|
|
120
|
+
this.log.write("info", `plan: removed "${id}"`);
|
|
121
|
+
else
|
|
122
|
+
this.log.write("info", `plan: no plan "${id}" to remove`);
|
|
123
|
+
process.exit(removed ? 0 : 1);
|
|
124
|
+
});
|
|
125
|
+
/* register CLI sub-command "ase plan purge" */
|
|
126
|
+
plan
|
|
127
|
+
.command("purge")
|
|
128
|
+
.description("Remove all plans with a modification time older than <days> (default: 31)")
|
|
129
|
+
.argument("[<days>]", "Maximum plan age in days", "31")
|
|
130
|
+
.action((days) => {
|
|
131
|
+
const n = Number.parseInt(days, 10);
|
|
132
|
+
if (!Number.isFinite(n) || n < 0)
|
|
133
|
+
throw new Error("plan: <days> must be a non-negative integer");
|
|
134
|
+
const removed = planPurge(n * 24 * 60 * 60 * 1000);
|
|
135
|
+
if (removed.length === 0)
|
|
136
|
+
this.log.write("info", "plan: no plans to purge");
|
|
137
|
+
else
|
|
138
|
+
for (const id of removed)
|
|
139
|
+
this.log.write("info", `plan: purged "${id}"`);
|
|
140
|
+
process.exit(0);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
package/dst/ase-service.js
CHANGED
|
@@ -10,14 +10,15 @@ import { fileURLToPath } from "node:url";
|
|
|
10
10
|
import { spawn } from "node:child_process";
|
|
11
11
|
import Hapi from "@hapi/hapi";
|
|
12
12
|
import axios from "axios";
|
|
13
|
-
import { isMap } from "yaml";
|
|
13
|
+
import { isMap, isScalar } from "yaml";
|
|
14
14
|
import * as v from "valibot";
|
|
15
15
|
import prettyMs from "pretty-ms";
|
|
16
16
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
17
17
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
18
18
|
import { z } from "zod";
|
|
19
|
-
import { Config, configSchema } from "./ase-config.js";
|
|
19
|
+
import { Config, configSchema, parseScope } from "./ase-config.js";
|
|
20
20
|
import { renderDiagram } from "./ase-diagram.js";
|
|
21
|
+
import { taskLoad, taskSave, taskDelete, taskList } from "./ase-task.js";
|
|
21
22
|
import pkg from "../package.json" with { type: "json" };
|
|
22
23
|
const SERVE_ENV = "ASE_SERVICE_SERVE";
|
|
23
24
|
const PORT_ENV = "ASE_SERVICE_PORT";
|
|
@@ -260,6 +261,198 @@ export default class ServiceCommand {
|
|
|
260
261
|
};
|
|
261
262
|
}
|
|
262
263
|
});
|
|
264
|
+
mcp.registerTool("task_list", {
|
|
265
|
+
title: "ASE task list",
|
|
266
|
+
description: "List all persisted task `id`s. " +
|
|
267
|
+
"Returns the ids as `text`, one per line, in lexicographic order; " +
|
|
268
|
+
"returns an empty string if no tasks exist.",
|
|
269
|
+
inputSchema: {}
|
|
270
|
+
}, async () => {
|
|
271
|
+
try {
|
|
272
|
+
const ids = taskList();
|
|
273
|
+
return {
|
|
274
|
+
content: [{ type: "text", text: ids.join("\n") }]
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
catch (err) {
|
|
278
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
279
|
+
return {
|
|
280
|
+
isError: true,
|
|
281
|
+
content: [{ type: "text", text: `task_list: ERROR: ${message}` }]
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
mcp.registerTool("task_load", {
|
|
286
|
+
title: "ASE task load",
|
|
287
|
+
description: "Load a previously persisted task by `id`. " +
|
|
288
|
+
"Returns the task as `text`; returns an empty string if no task exists for the `id`.",
|
|
289
|
+
inputSchema: {
|
|
290
|
+
id: z.string()
|
|
291
|
+
.describe("task identifier (allowed characters: A-Z, a-z, 0-9, '-')")
|
|
292
|
+
}
|
|
293
|
+
}, async (args) => {
|
|
294
|
+
try {
|
|
295
|
+
const text = taskLoad(args.id);
|
|
296
|
+
return {
|
|
297
|
+
content: [{ type: "text", text }]
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
catch (err) {
|
|
301
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
302
|
+
return {
|
|
303
|
+
isError: true,
|
|
304
|
+
content: [{ type: "text", text: `task_load: ERROR: ${message}` }]
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
mcp.registerTool("task_save", {
|
|
309
|
+
title: "ASE task save",
|
|
310
|
+
description: "Persist a task as `text` under `id`. " +
|
|
311
|
+
"Overwrites any existing task for the same `id`.",
|
|
312
|
+
inputSchema: {
|
|
313
|
+
id: z.string()
|
|
314
|
+
.describe("task identifier (allowed characters: A-Z, a-z, 0-9, '-')"),
|
|
315
|
+
text: z.string()
|
|
316
|
+
.describe("text content of the task")
|
|
317
|
+
}
|
|
318
|
+
}, async (args) => {
|
|
319
|
+
try {
|
|
320
|
+
taskSave(args.id, args.text);
|
|
321
|
+
return {
|
|
322
|
+
content: [{ type: "text", text: `task_save: OK: saved task "${args.id}"` }]
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
catch (err) {
|
|
326
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
327
|
+
return {
|
|
328
|
+
isError: true,
|
|
329
|
+
content: [{ type: "text", text: `task_save: FAILED: ${message}` }]
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
mcp.registerTool("task_delete", {
|
|
334
|
+
title: "ASE task delete",
|
|
335
|
+
description: "Delete a previously persisted task by `id`. " +
|
|
336
|
+
"Returns a status `text` indicating whether a task existed and was removed.",
|
|
337
|
+
inputSchema: {
|
|
338
|
+
id: z.string()
|
|
339
|
+
.describe("task identifier (allowed characters: A-Z, a-z, 0-9, '-')")
|
|
340
|
+
}
|
|
341
|
+
}, async (args) => {
|
|
342
|
+
try {
|
|
343
|
+
const removed = taskDelete(args.id);
|
|
344
|
+
const msg = removed ?
|
|
345
|
+
`task_delete: OK: removed task "${args.id}"` :
|
|
346
|
+
`task_delete: WARNING: no task "${args.id}" to remove`;
|
|
347
|
+
return {
|
|
348
|
+
content: [{ type: "text", text: msg }]
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
catch (err) {
|
|
352
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
353
|
+
return {
|
|
354
|
+
isError: true,
|
|
355
|
+
content: [{ type: "text", text: `task_delete: ERROR: ${message}` }]
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
mcp.registerTool("persona", {
|
|
360
|
+
title: "ASE persona style get/set",
|
|
361
|
+
description: "Get or set the active ASE agent persona `style`. " +
|
|
362
|
+
"If `style` is provided, it sets the persona style, " +
|
|
363
|
+
"otherwise it returns the current persona `style`. " +
|
|
364
|
+
"If `session` is provided, the operation is scoped to that session, " +
|
|
365
|
+
"otherwise it operates on the broadest scope (user/project cascade). " +
|
|
366
|
+
"Allowed styles: \"writer\" (decorative, eloquent, explaining), " +
|
|
367
|
+
"\"engineer\" (brief, factual, accurate), " +
|
|
368
|
+
"\"telegrapher\" (very brief, factual, abbreviating), " +
|
|
369
|
+
"\"caveman\" (ultra brief, rough, stuttering).",
|
|
370
|
+
inputSchema: {
|
|
371
|
+
style: z.enum(["writer", "engineer", "telegrapher", "caveman"]).optional()
|
|
372
|
+
.describe("persona style to set; if omitted, the current persona style is returned"),
|
|
373
|
+
session: z.string().optional()
|
|
374
|
+
.describe("session identifier (allowed characters: A-Z, a-z, 0-9, '-'); " +
|
|
375
|
+
"if omitted, the operation is not scoped to a specific session")
|
|
376
|
+
}
|
|
377
|
+
}, async (args) => {
|
|
378
|
+
try {
|
|
379
|
+
const scope = args.session !== undefined ?
|
|
380
|
+
parseScope(`session:${args.session}`) :
|
|
381
|
+
parseScope(undefined);
|
|
382
|
+
const cfg = new Config("config", configSchema, this.log, scope);
|
|
383
|
+
cfg.read();
|
|
384
|
+
if (args.style !== undefined) {
|
|
385
|
+
cfg.set("agent.persona", args.style);
|
|
386
|
+
cfg.write();
|
|
387
|
+
const where = args.session !== undefined ?
|
|
388
|
+
` for session "${args.session}"` : "";
|
|
389
|
+
const msg = `persona: OK: set agent.persona to "${args.style}"${where}`;
|
|
390
|
+
return {
|
|
391
|
+
content: [{ type: "text", text: msg }]
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
const val = cfg.get("agent.persona");
|
|
395
|
+
if (val === undefined)
|
|
396
|
+
return {
|
|
397
|
+
content: [{ type: "text", text: "" }]
|
|
398
|
+
};
|
|
399
|
+
const text = String(isScalar(val) ? val.value : val);
|
|
400
|
+
return {
|
|
401
|
+
content: [{ type: "text", text }]
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
catch (err) {
|
|
405
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
406
|
+
return {
|
|
407
|
+
isError: true,
|
|
408
|
+
content: [{ type: "text", text: `persona: ERROR: ${message}` }]
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
mcp.registerTool("task_id", {
|
|
413
|
+
title: "ASE task id get/set",
|
|
414
|
+
description: "Get or set the active ASE task `id` for a given `session`. " +
|
|
415
|
+
"If `id` is provided, it set the task id in the given `session`, " +
|
|
416
|
+
"otherwise it returns the current task `id` of the `session`.",
|
|
417
|
+
inputSchema: {
|
|
418
|
+
id: z.string().optional()
|
|
419
|
+
.describe("task identifier to set (allowed characters: A-Z, a-z, 0-9, '-'); " +
|
|
420
|
+
"if omitted, the current task id is returned"),
|
|
421
|
+
session: z.string()
|
|
422
|
+
.describe("session identifier (allowed characters: A-Z, a-z, 0-9, '-')")
|
|
423
|
+
}
|
|
424
|
+
}, async (args) => {
|
|
425
|
+
try {
|
|
426
|
+
const scope = parseScope(`session:${args.session}`);
|
|
427
|
+
const cfg = new Config("config", configSchema, this.log, scope);
|
|
428
|
+
cfg.read();
|
|
429
|
+
if (args.id !== undefined) {
|
|
430
|
+
cfg.set("agent.task", args.id);
|
|
431
|
+
cfg.write();
|
|
432
|
+
const msg = `task_id: OK: set agent.task to "${args.id}" ` +
|
|
433
|
+
`for session "${args.session}"`;
|
|
434
|
+
return {
|
|
435
|
+
content: [{ type: "text", text: msg }]
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
const val = cfg.get("agent.task");
|
|
439
|
+
if (val === undefined)
|
|
440
|
+
return {
|
|
441
|
+
content: [{ type: "text", text: "" }]
|
|
442
|
+
};
|
|
443
|
+
const text = String(isScalar(val) ? val.value : val);
|
|
444
|
+
return {
|
|
445
|
+
content: [{ type: "text", text }]
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
catch (err) {
|
|
449
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
450
|
+
return {
|
|
451
|
+
isError: true,
|
|
452
|
+
content: [{ type: "text", text: `task_id: ERROR: ${message}` }]
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
});
|
|
263
456
|
return mcp;
|
|
264
457
|
};
|
|
265
458
|
/* listen to HTTP/REST endpoints */
|
package/dst/ase-setup.js
CHANGED
|
@@ -21,9 +21,14 @@ export default class SetupCommand {
|
|
|
21
21
|
});
|
|
22
22
|
}
|
|
23
23
|
/* run a sub-process, suppressing output on success and emitting it on failure */
|
|
24
|
-
async run(cmd, args,
|
|
24
|
+
async run(cmd, args, opts = {}) {
|
|
25
|
+
const { cwd, quiet = false } = opts;
|
|
25
26
|
this.log.write("info", `setup: $ ${cmd} ${args.join(" ")}` +
|
|
26
27
|
(cwd !== undefined ? ` (cwd: ${cwd})` : ""));
|
|
28
|
+
if (quiet) {
|
|
29
|
+
await execa(cmd, args, { stdio: "ignore", cwd, reject: false });
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
27
32
|
await execa(cmd, args, { stdio: "pipe", cwd }).catch((err) => {
|
|
28
33
|
const exitCode = typeof err?.exitCode === "number" ? err.exitCode : -1;
|
|
29
34
|
this.log.write("error", `setup: command failed: exit code: ${exitCode}`);
|
|
@@ -53,12 +58,16 @@ export default class SetupCommand {
|
|
|
53
58
|
async doUpdate(force, dev) {
|
|
54
59
|
await this.ensureTool("npm");
|
|
55
60
|
await this.ensureTool("claude");
|
|
61
|
+
/* best-effort stop of background service */
|
|
62
|
+
this.log.write("info", `setup: update${dev ? "[dev]" : ""}: ` +
|
|
63
|
+
"stopping potentially running ASE service");
|
|
64
|
+
await this.run("ase", ["service", "stop"], { quiet: true });
|
|
56
65
|
if (dev) {
|
|
57
66
|
/* update ASE CLI Tool */
|
|
58
67
|
this.log.write("info", "setup: update[dev]: re-build ASE CLI tool (origin: local)");
|
|
59
68
|
const tooldir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
60
|
-
await this.run("npm", ["install"], tooldir);
|
|
61
|
-
await this.run("npm", ["start", "build"], tooldir);
|
|
69
|
+
await this.run("npm", ["install"], { cwd: tooldir });
|
|
70
|
+
await this.run("npm", ["start", "build"], { cwd: tooldir });
|
|
62
71
|
/* in development mode the local plugin files are already current
|
|
63
72
|
but there is no version change in the plugin manifest,
|
|
64
73
|
so just re-install the plugin to let Claude Code update its copy */
|
|
@@ -88,6 +97,10 @@ export default class SetupCommand {
|
|
|
88
97
|
async doUninstall(dev) {
|
|
89
98
|
await this.ensureTool("npm");
|
|
90
99
|
await this.ensureTool("claude");
|
|
100
|
+
/* best-effort stop of background service */
|
|
101
|
+
this.log.write("info", `setup: uninstall${dev ? "[dev]" : ""}: ` +
|
|
102
|
+
"stopping potentially running ASE service");
|
|
103
|
+
await this.run("ase", ["service", "stop"], { quiet: true });
|
|
91
104
|
/* uninstall ASE Claude Code plugin */
|
|
92
105
|
this.log.write("info", `setup: uninstall${dev ? "[dev]" : ""}: ` +
|
|
93
106
|
`uninstalling ASE Claude Code plugin (origin: ${dev ? "local" : "remote"})`);
|
|
@@ -0,0 +1,134 @@
|
|
|
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 fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { execFileSync } from "node:child_process";
|
|
9
|
+
import { execaSync } from "execa";
|
|
10
|
+
import { Config, configSchema, parseScope } from "./ase-config.js";
|
|
11
|
+
/* read stdin into a single string */
|
|
12
|
+
const readStdin = async () => {
|
|
13
|
+
const chunks = [];
|
|
14
|
+
for await (const chunk of process.stdin)
|
|
15
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
16
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
17
|
+
};
|
|
18
|
+
/* detect terminal column width via /dev/tty (stdout is a pipe under Claude Code) */
|
|
19
|
+
const detectTermWidth = () => {
|
|
20
|
+
let width = 0;
|
|
21
|
+
try {
|
|
22
|
+
const tty = fs.openSync("/dev/tty", "r");
|
|
23
|
+
const out = execFileSync("tput", ["cols"], { stdio: [tty, "pipe", "ignore"] });
|
|
24
|
+
fs.closeSync(tty);
|
|
25
|
+
width = parseInt(out.toString("utf8").trim()) || 0;
|
|
26
|
+
}
|
|
27
|
+
catch (_e) {
|
|
28
|
+
/* no controlling terminal */
|
|
29
|
+
}
|
|
30
|
+
return width;
|
|
31
|
+
};
|
|
32
|
+
/* command-line handling */
|
|
33
|
+
export default class StatuslineCommand {
|
|
34
|
+
log;
|
|
35
|
+
constructor(log) {
|
|
36
|
+
this.log = log;
|
|
37
|
+
}
|
|
38
|
+
/* register commands */
|
|
39
|
+
register(program) {
|
|
40
|
+
program
|
|
41
|
+
.command("statusline")
|
|
42
|
+
.description("Render Claude Code statusline from stdin JSON")
|
|
43
|
+
.action(async () => {
|
|
44
|
+
/* read all of stdin */
|
|
45
|
+
const input = await readStdin();
|
|
46
|
+
/* parse JSON data */
|
|
47
|
+
let data;
|
|
48
|
+
try {
|
|
49
|
+
data = JSON.parse(input);
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
53
|
+
this.log.write("error", `statusline: invalid JSON on stdin: ${message}`);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
/* fetch information from data */
|
|
57
|
+
const dir = path.basename(data.workspace?.current_dir ?? "");
|
|
58
|
+
const model = data.model?.display_name ?? "";
|
|
59
|
+
const pct = Math.floor(data.context_window?.used_percentage ?? 0);
|
|
60
|
+
const effort = data.effort?.level ?? "unknown";
|
|
61
|
+
const thinking = (data.thinking?.enabled ?? false) === true ? "yes" : "no";
|
|
62
|
+
const sessionId = data.session_id ?? "unknown";
|
|
63
|
+
/* optionally determine ASE task id and persona style via in-process Config */
|
|
64
|
+
let taskId = process.env.ASE_TASK_ID ?? "";
|
|
65
|
+
let persona = process.env.ASE_PERSONA_STYLE ?? "";
|
|
66
|
+
try {
|
|
67
|
+
const cfg = new Config("config", configSchema, this.log, parseScope(`session:${sessionId}`));
|
|
68
|
+
cfg.read("lenient");
|
|
69
|
+
const t = String(cfg.get("agent.task") ?? "").trim();
|
|
70
|
+
const p = String(cfg.get("agent.persona") ?? "").trim();
|
|
71
|
+
if (t !== "")
|
|
72
|
+
taskId = t;
|
|
73
|
+
if (p !== "")
|
|
74
|
+
persona = p;
|
|
75
|
+
}
|
|
76
|
+
catch (_e) {
|
|
77
|
+
/* cascade unavailable; keep env-var fallbacks */
|
|
78
|
+
}
|
|
79
|
+
/* optionally determine terminal width */
|
|
80
|
+
const width = detectTermWidth();
|
|
81
|
+
/* configure ANSI sequences */
|
|
82
|
+
const RESET = "\x1b[0m";
|
|
83
|
+
const BOLD = "\x1b[1m";
|
|
84
|
+
const BLACK = "\x1b[30m";
|
|
85
|
+
const BLUE = "\x1b[34m";
|
|
86
|
+
const YELLOW = "\x1b[33m";
|
|
87
|
+
const RED = "\x1b[31m";
|
|
88
|
+
/* determine context bar information */
|
|
89
|
+
const barSize = 20;
|
|
90
|
+
const barColor = pct >= 80 ? RED : pct >= 60 ? YELLOW : pct >= 40 ? BLUE : RESET;
|
|
91
|
+
const filled = Math.round(pct / 100 * barSize);
|
|
92
|
+
const bar = "█".repeat(filled) + "░".repeat(barSize - filled);
|
|
93
|
+
/* generate output */
|
|
94
|
+
let output = "";
|
|
95
|
+
output += `${BLUE}※ user: ${BOLD}${process.env.USER ?? process.env.LOGNAME ?? "unknown"}${RESET} `;
|
|
96
|
+
if (width > 0 && width < 30)
|
|
97
|
+
output += "\n";
|
|
98
|
+
output += `${RED}⚑ project: ${BOLD}${dir}${RESET} `;
|
|
99
|
+
if (width > 0 && width < 60)
|
|
100
|
+
output += "\n";
|
|
101
|
+
if (taskId !== "") {
|
|
102
|
+
output += `${BLACK}◉ task: ${BOLD}${taskId}${RESET} `;
|
|
103
|
+
if (width > 0 && width < 90)
|
|
104
|
+
output += "\n";
|
|
105
|
+
}
|
|
106
|
+
output += `⏻ session: ${BOLD}${sessionId}${RESET}\n`;
|
|
107
|
+
output += `⚙ model: ${BOLD}${model}${RESET} `;
|
|
108
|
+
if (width > 0 && width < 30)
|
|
109
|
+
output += "\n";
|
|
110
|
+
output += `⚒ effort: ${BOLD}${effort}${RESET} `;
|
|
111
|
+
if (width > 0 && width < 60)
|
|
112
|
+
output += "\n";
|
|
113
|
+
output += `⚛ thinking: ${BOLD}${thinking}${RESET}\n`;
|
|
114
|
+
if (persona !== "") {
|
|
115
|
+
output += `☯ persona: ${BOLD}${persona}${RESET} `;
|
|
116
|
+
if (width > 0 && width < 30)
|
|
117
|
+
output += "\n";
|
|
118
|
+
}
|
|
119
|
+
output += `${barColor}◔ context: ${bar} ${pct}%${RESET}\n`;
|
|
120
|
+
/* send output */
|
|
121
|
+
process.stdout.write(output);
|
|
122
|
+
/* optionally publish task id to the calling tmux pane as a per-pane user
|
|
123
|
+
option, so someone (like claudeX) can pick it up via #{@ase_task_id} */
|
|
124
|
+
if (process.env.TMUX !== undefined
|
|
125
|
+
&& process.env.TMUX !== ""
|
|
126
|
+
&& process.env.TMUX_PANE !== undefined
|
|
127
|
+
&& process.env.TMUX_PANE !== "") {
|
|
128
|
+
const tid = taskId !== "" ? taskId : "default";
|
|
129
|
+
execaSync("tmux", ["set-option", "-p", "-t", process.env.TMUX_PANE,
|
|
130
|
+
"@ase_task_id", tid], { stdio: "ignore", reject: false });
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
package/dst/ase-task.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
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 path from "node:path";
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import { execaSync } from "execa";
|
|
10
|
+
/* validate the task id to keep it safe as a filename component */
|
|
11
|
+
const validateId = (id) => {
|
|
12
|
+
if (typeof id !== "string" || id.length === 0)
|
|
13
|
+
throw new Error("task: id must be a non-empty string");
|
|
14
|
+
if (!/^[A-Za-z0-9-]+$/.test(id))
|
|
15
|
+
throw new Error("task: id must match [A-Za-z0-9-]+");
|
|
16
|
+
};
|
|
17
|
+
/* resolve the on-disk path for a given task id */
|
|
18
|
+
const taskPath = (id) => {
|
|
19
|
+
validateId(id);
|
|
20
|
+
return path.join(os.homedir(), ".ase", "task", id, "plan.md");
|
|
21
|
+
};
|
|
22
|
+
/* load a task; returns empty string if no task exists */
|
|
23
|
+
export const taskLoad = (id) => {
|
|
24
|
+
const file = taskPath(id);
|
|
25
|
+
if (!fs.existsSync(file))
|
|
26
|
+
return "";
|
|
27
|
+
return fs.readFileSync(file, "utf8");
|
|
28
|
+
};
|
|
29
|
+
/* save a task as UTF-8 text under the given id; the task's home
|
|
30
|
+
directory ~/.ase/task/<id>/ is owned by ASE and removed in full
|
|
31
|
+
by taskDelete, so callers must not place foreign files there */
|
|
32
|
+
export const taskSave = (id, text) => {
|
|
33
|
+
if (typeof text !== "string")
|
|
34
|
+
throw new Error("task: text must be a string");
|
|
35
|
+
const file = taskPath(id);
|
|
36
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
37
|
+
fs.writeFileSync(file, text, "utf8");
|
|
38
|
+
};
|
|
39
|
+
/* delete a task by id; removes the entire task home directory
|
|
40
|
+
~/.ase/task/<id>/ (owned by ASE); returns true if a task existed */
|
|
41
|
+
export const taskDelete = (id) => {
|
|
42
|
+
const file = taskPath(id);
|
|
43
|
+
if (!fs.existsSync(file))
|
|
44
|
+
return false;
|
|
45
|
+
fs.rmSync(path.dirname(file), { recursive: true, force: true });
|
|
46
|
+
return true;
|
|
47
|
+
};
|
|
48
|
+
/* list all persisted task ids in lexicographic order */
|
|
49
|
+
export const taskList = () => {
|
|
50
|
+
const dir = path.join(os.homedir(), ".ase", "task");
|
|
51
|
+
if (!fs.existsSync(dir))
|
|
52
|
+
return [];
|
|
53
|
+
const ids = [];
|
|
54
|
+
for (const entry of fs.readdirSync(dir)) {
|
|
55
|
+
if (!/^[A-Za-z0-9-]+$/.test(entry))
|
|
56
|
+
continue;
|
|
57
|
+
const file = path.join(dir, entry, "plan.md");
|
|
58
|
+
if (!fs.existsSync(file))
|
|
59
|
+
continue;
|
|
60
|
+
const st = fs.statSync(file);
|
|
61
|
+
if (!st.isFile())
|
|
62
|
+
continue;
|
|
63
|
+
ids.push(entry);
|
|
64
|
+
}
|
|
65
|
+
ids.sort();
|
|
66
|
+
return ids;
|
|
67
|
+
};
|
|
68
|
+
/* purge tasks whose modification time is older than the given cutoff in
|
|
69
|
+
milliseconds; returns the list of removed task ids */
|
|
70
|
+
export const taskPurge = (maxAgeMs) => {
|
|
71
|
+
const dir = path.join(os.homedir(), ".ase", "task");
|
|
72
|
+
if (!fs.existsSync(dir))
|
|
73
|
+
return [];
|
|
74
|
+
const cutoff = Date.now() - maxAgeMs;
|
|
75
|
+
const removed = [];
|
|
76
|
+
for (const entry of fs.readdirSync(dir)) {
|
|
77
|
+
if (!/^[A-Za-z0-9-]+$/.test(entry))
|
|
78
|
+
continue;
|
|
79
|
+
const sub = path.join(dir, entry);
|
|
80
|
+
const file = path.join(sub, "plan.md");
|
|
81
|
+
if (!fs.existsSync(file))
|
|
82
|
+
continue;
|
|
83
|
+
const st = fs.statSync(file);
|
|
84
|
+
if (!st.isFile())
|
|
85
|
+
continue;
|
|
86
|
+
if (st.mtimeMs < cutoff) {
|
|
87
|
+
fs.rmSync(sub, { recursive: true, force: true });
|
|
88
|
+
removed.push(entry);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return removed;
|
|
92
|
+
};
|
|
93
|
+
/* read all of stdin as a UTF-8 string */
|
|
94
|
+
const readStdin = () => {
|
|
95
|
+
return new Promise((resolve, reject) => {
|
|
96
|
+
const chunks = [];
|
|
97
|
+
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
98
|
+
process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
99
|
+
process.stdin.on("error", (err) => reject(err));
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
/* CLI command "ase task" */
|
|
103
|
+
export default class TaskCommand {
|
|
104
|
+
log;
|
|
105
|
+
constructor(log) {
|
|
106
|
+
this.log = log;
|
|
107
|
+
}
|
|
108
|
+
/* register commands */
|
|
109
|
+
register(program) {
|
|
110
|
+
/* register CLI top-level command "ase task" */
|
|
111
|
+
const task = program
|
|
112
|
+
.command("task")
|
|
113
|
+
.description("Manage persisted tasks under ~/.ase/task/<id>/plan.md")
|
|
114
|
+
.action(() => {
|
|
115
|
+
task.outputHelp();
|
|
116
|
+
process.exit(1);
|
|
117
|
+
});
|
|
118
|
+
/* register CLI sub-command "ase task list" */
|
|
119
|
+
task
|
|
120
|
+
.command("list")
|
|
121
|
+
.description("List all persisted task ids, one per line")
|
|
122
|
+
.action(() => {
|
|
123
|
+
const ids = taskList();
|
|
124
|
+
for (const id of ids)
|
|
125
|
+
process.stdout.write(`${id}\n`);
|
|
126
|
+
process.exit(0);
|
|
127
|
+
});
|
|
128
|
+
/* register CLI sub-command "ase task load" */
|
|
129
|
+
task
|
|
130
|
+
.command("load")
|
|
131
|
+
.description("Load a task by id and write it to stdout")
|
|
132
|
+
.argument("<id>", "Task identifier")
|
|
133
|
+
.action((id) => {
|
|
134
|
+
const text = taskLoad(id);
|
|
135
|
+
process.stdout.write(text);
|
|
136
|
+
process.exit(0);
|
|
137
|
+
});
|
|
138
|
+
/* register CLI sub-command "ase task edit" */
|
|
139
|
+
task
|
|
140
|
+
.command("edit")
|
|
141
|
+
.description("Edit a task by id with $EDITOR")
|
|
142
|
+
.argument("<id>", "Task identifier")
|
|
143
|
+
.action((id) => {
|
|
144
|
+
const file = taskPath(id);
|
|
145
|
+
const editor = process.env.EDITOR ?? process.env.VISUAL ?? "vi";
|
|
146
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
147
|
+
if (!fs.existsSync(file))
|
|
148
|
+
fs.writeFileSync(file, "", "utf8");
|
|
149
|
+
execaSync(editor, [file], { stdio: "inherit" });
|
|
150
|
+
this.log.write("info", `task: edited "${id}"`);
|
|
151
|
+
process.exit(0);
|
|
152
|
+
});
|
|
153
|
+
/* register CLI sub-command "ase task save" */
|
|
154
|
+
task
|
|
155
|
+
.command("save")
|
|
156
|
+
.description("Save a task by id, reading content from stdin")
|
|
157
|
+
.argument("<id>", "Task identifier")
|
|
158
|
+
.action(async (id) => {
|
|
159
|
+
const text = await readStdin();
|
|
160
|
+
taskSave(id, text);
|
|
161
|
+
this.log.write("info", `task: saved "${id}"`);
|
|
162
|
+
process.exit(0);
|
|
163
|
+
});
|
|
164
|
+
/* register CLI sub-command "ase task delete" */
|
|
165
|
+
task
|
|
166
|
+
.command("delete")
|
|
167
|
+
.description("Delete a task by id")
|
|
168
|
+
.argument("<id>", "Task identifier")
|
|
169
|
+
.action((id) => {
|
|
170
|
+
const removed = taskDelete(id);
|
|
171
|
+
if (removed)
|
|
172
|
+
this.log.write("info", `task: removed "${id}"`);
|
|
173
|
+
else
|
|
174
|
+
this.log.write("info", `task: no task "${id}" to remove`);
|
|
175
|
+
process.exit(removed ? 0 : 1);
|
|
176
|
+
});
|
|
177
|
+
/* register CLI sub-command "ase task purge" */
|
|
178
|
+
task
|
|
179
|
+
.command("purge")
|
|
180
|
+
.description("Remove all tasks with a modification time older than <days> (default: 31)")
|
|
181
|
+
.argument("[<days>]", "Maximum task age in days", "31")
|
|
182
|
+
.action((days) => {
|
|
183
|
+
const n = Number.parseInt(days, 10);
|
|
184
|
+
if (!Number.isFinite(n) || n < 0)
|
|
185
|
+
throw new Error("task: <days> must be a non-negative integer");
|
|
186
|
+
const removed = taskPurge(n * 24 * 60 * 60 * 1000);
|
|
187
|
+
if (removed.length === 0)
|
|
188
|
+
this.log.write("info", "task: no tasks to purge");
|
|
189
|
+
else
|
|
190
|
+
for (const id of removed)
|
|
191
|
+
this.log.write("info", `task: purged "${id}"`);
|
|
192
|
+
process.exit(0);
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
package/dst/ase.js
CHANGED
|
@@ -12,6 +12,8 @@ import MCPCommand from "./ase-mcp.js";
|
|
|
12
12
|
import HookCommand from "./ase-hook.js";
|
|
13
13
|
import DiagramCommand from "./ase-diagram.js";
|
|
14
14
|
import SetupCommand from "./ase-setup.js";
|
|
15
|
+
import StatuslineCommand from "./ase-statusline.js";
|
|
16
|
+
import TaskCommand from "./ase-task.js";
|
|
15
17
|
import pkg from "../package.json" with { type: "json" };
|
|
16
18
|
/* globally initialize logger */
|
|
17
19
|
const log = new Log("ase", "warning", "-");
|
|
@@ -46,6 +48,8 @@ const main = async () => {
|
|
|
46
48
|
new HookCommand(log).register(program);
|
|
47
49
|
new DiagramCommand(log).register(program);
|
|
48
50
|
new SetupCommand(log).register(program);
|
|
51
|
+
new StatuslineCommand(log).register(program);
|
|
52
|
+
new TaskCommand(log).register(program);
|
|
49
53
|
/* parse program arguments */
|
|
50
54
|
await program.parseAsync(process.argv);
|
|
51
55
|
/* gracefully terminate */
|
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.19",
|
|
10
10
|
"license": "GPL-3.0-only",
|
|
11
11
|
"author": {
|
|
12
12
|
"name": "Dr. Ralf S. Engelschall",
|