@rse/ase 0.0.27 → 0.0.29
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 +41 -17
- package/dst/ase-diagram.js +184 -115
- package/dst/ase-foo.js +21 -0
- package/dst/ase-hook.js +41 -18
- package/dst/ase-kv.js +136 -0
- package/dst/ase-mcp.js +13 -8
- package/dst/ase-persona.js +89 -0
- package/dst/ase-service.js +184 -407
- package/dst/ase-task.js +307 -95
- package/dst/ase-timestamp.js +36 -0
- package/dst/ase.js +3 -3
- package/package.json +9 -3
package/dst/ase-kv.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
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: in-memory key/value store living inside the
|
|
8
|
+
"ase service" process; per-project (one service per project) and
|
|
9
|
+
not persisted; intended for sharing information between skills
|
|
10
|
+
across multiple Claude Code instances connected to the same service */
|
|
11
|
+
export class KV {
|
|
12
|
+
/* the actual in-memory store */
|
|
13
|
+
static store = new Map();
|
|
14
|
+
/* maximum allowed key length, to keep memory bounded */
|
|
15
|
+
static KEY_MAX_LEN = 1024;
|
|
16
|
+
/* validate the key to keep it non-empty and bounded in length */
|
|
17
|
+
static validateKey(key) {
|
|
18
|
+
if (typeof key !== "string" || key.length === 0)
|
|
19
|
+
throw new Error("kv: key must be a non-empty string");
|
|
20
|
+
if (key.length > KV.KEY_MAX_LEN)
|
|
21
|
+
throw new Error(`kv: key must be no longer than ${KV.KEY_MAX_LEN} characters`);
|
|
22
|
+
}
|
|
23
|
+
/* test whether a value is stored under the given key */
|
|
24
|
+
static has(key) {
|
|
25
|
+
KV.validateKey(key);
|
|
26
|
+
return KV.store.has(key);
|
|
27
|
+
}
|
|
28
|
+
/* get a value by key; returns undefined if no value is stored */
|
|
29
|
+
static get(key) {
|
|
30
|
+
KV.validateKey(key);
|
|
31
|
+
return KV.store.get(key);
|
|
32
|
+
}
|
|
33
|
+
/* set a value under the given key; overwrites any existing value */
|
|
34
|
+
static set(key, val) {
|
|
35
|
+
KV.validateKey(key);
|
|
36
|
+
KV.store.set(key, val);
|
|
37
|
+
}
|
|
38
|
+
/* delete a value by key; returns true if a value existed */
|
|
39
|
+
static delete(key) {
|
|
40
|
+
KV.validateKey(key);
|
|
41
|
+
return KV.store.delete(key);
|
|
42
|
+
}
|
|
43
|
+
/* clear all keys; returns the number of keys removed */
|
|
44
|
+
static clear() {
|
|
45
|
+
const n = KV.store.size;
|
|
46
|
+
KV.store.clear();
|
|
47
|
+
return n;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/* MCP registration entry point for in-memory key/value tools */
|
|
51
|
+
export class KVMCP {
|
|
52
|
+
register(mcp) {
|
|
53
|
+
/* key/value get */
|
|
54
|
+
mcp.registerTool("kv_get", {
|
|
55
|
+
title: "ASE key/value get",
|
|
56
|
+
description: "Get a value from the in-memory key/value store by `key`. " +
|
|
57
|
+
"Returns the value as JSON-encoded `text`; returns an empty string if no value is stored.",
|
|
58
|
+
inputSchema: {
|
|
59
|
+
key: z.string()
|
|
60
|
+
.describe("key identifier (non-empty string, up to 1024 characters)")
|
|
61
|
+
}
|
|
62
|
+
}, async (args) => {
|
|
63
|
+
try {
|
|
64
|
+
if (!KV.has(args.key))
|
|
65
|
+
return { content: [{ type: "text", text: "" }] };
|
|
66
|
+
const val = KV.get(args.key);
|
|
67
|
+
const text = JSON.stringify(val);
|
|
68
|
+
return { content: [{ type: "text", text }] };
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
72
|
+
return { isError: true, content: [{ type: "text", text: `kv_get: ERROR: ${message}` }] };
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
/* key/value set */
|
|
76
|
+
mcp.registerTool("kv_set", {
|
|
77
|
+
title: "ASE key/value set",
|
|
78
|
+
description: "Store a `val` under the given `key` in the in-memory key/value store. " +
|
|
79
|
+
"Overwrites any existing value for the same `key`. " +
|
|
80
|
+
"The value can be any JSON-compatible type (string, number, boolean, null, array, object).",
|
|
81
|
+
inputSchema: {
|
|
82
|
+
key: z.string()
|
|
83
|
+
.describe("key identifier (non-empty string, up to 1024 characters)"),
|
|
84
|
+
val: z.any()
|
|
85
|
+
.describe("arbitrary JSON-compatible value to store under `key`")
|
|
86
|
+
}
|
|
87
|
+
}, async (args) => {
|
|
88
|
+
try {
|
|
89
|
+
KV.set(args.key, args.val);
|
|
90
|
+
return { content: [{ type: "text", text: `kv_set: OK: stored key "${args.key}"` }] };
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
94
|
+
return { isError: true, content: [{ type: "text", text: `kv_set: ERROR: ${message}` }] };
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
/* key/value clear */
|
|
98
|
+
mcp.registerTool("kv_clear", {
|
|
99
|
+
title: "ASE key/value clear",
|
|
100
|
+
description: "Remove all keys from the in-memory key/value store. " +
|
|
101
|
+
"Returns a status `text` indicating how many keys were removed.",
|
|
102
|
+
inputSchema: {}
|
|
103
|
+
}, async () => {
|
|
104
|
+
try {
|
|
105
|
+
const n = KV.clear();
|
|
106
|
+
return { content: [{ type: "text", text: `kv_clear: OK: removed ${n} key(s)` }] };
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
110
|
+
return { isError: true, content: [{ type: "text", text: `kv_clear: ERROR: ${message}` }] };
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
/* key/value delete */
|
|
114
|
+
mcp.registerTool("kv_delete", {
|
|
115
|
+
title: "ASE key/value delete",
|
|
116
|
+
description: "Delete a value from the in-memory key/value store by `key`. " +
|
|
117
|
+
"Returns a status `text` indicating whether a value existed and was removed.",
|
|
118
|
+
inputSchema: {
|
|
119
|
+
key: z.string()
|
|
120
|
+
.describe("key identifier (non-empty string, up to 1024 characters)")
|
|
121
|
+
}
|
|
122
|
+
}, async (args) => {
|
|
123
|
+
try {
|
|
124
|
+
const removed = KV.delete(args.key);
|
|
125
|
+
const msg = removed ?
|
|
126
|
+
`kv_delete: OK: removed key "${args.key}"` :
|
|
127
|
+
`kv_delete: WARNING: no key "${args.key}" to remove`;
|
|
128
|
+
return { content: [{ type: "text", text: msg }] };
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
132
|
+
return { isError: true, content: [{ type: "text", text: `kv_delete: ERROR: ${message}` }] };
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
package/dst/ase-mcp.js
CHANGED
|
@@ -9,7 +9,7 @@ import { execa } from "execa";
|
|
|
9
9
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
10
10
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
11
11
|
import { Config, configSchema } from "./ase-config.js";
|
|
12
|
-
import { SERVICE_HOST as HOST, serviceSchema, probe } from "./ase-service
|
|
12
|
+
import { SERVICE_HOST as HOST, serviceSchema, probe } from "./ase-service.js";
|
|
13
13
|
/* CLI command "ase mcp" */
|
|
14
14
|
export default class MCPCommand {
|
|
15
15
|
log;
|
|
@@ -68,10 +68,12 @@ export default class MCPCommand {
|
|
|
68
68
|
return;
|
|
69
69
|
bridgeDone = true;
|
|
70
70
|
closedByUs = true;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
client?.close()
|
|
71
|
+
const timeout = new Promise((resolve) => setTimeout(resolve, 3000));
|
|
72
|
+
await Promise.race([
|
|
73
|
+
Promise.allSettled([server.close(), client?.close()]),
|
|
74
|
+
timeout
|
|
74
75
|
]);
|
|
76
|
+
process.exit(0);
|
|
75
77
|
};
|
|
76
78
|
/* (re-)connect the HTTP client to the service */
|
|
77
79
|
const connectClient = async () => {
|
|
@@ -97,11 +99,13 @@ export default class MCPCommand {
|
|
|
97
99
|
await next.start();
|
|
98
100
|
};
|
|
99
101
|
/* reconnect loop: restart service if needed, then reconnect client */
|
|
100
|
-
const reconnect = async (attempt = 0) => {
|
|
102
|
+
const reconnect = async (attempt = 0, done) => {
|
|
101
103
|
const delay = Math.min(500 * 2 ** attempt, 10000);
|
|
102
104
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
103
|
-
if (bridgeDone)
|
|
105
|
+
if (bridgeDone) {
|
|
106
|
+
done?.();
|
|
104
107
|
return;
|
|
108
|
+
}
|
|
105
109
|
try {
|
|
106
110
|
const ctx = await this.ensureService();
|
|
107
111
|
port = ctx.port;
|
|
@@ -110,11 +114,12 @@ export default class MCPCommand {
|
|
|
110
114
|
closedByUs = false;
|
|
111
115
|
await connectClient();
|
|
112
116
|
this.log.write("info", "mcp: reconnected to service");
|
|
117
|
+
done?.();
|
|
113
118
|
}
|
|
114
119
|
catch (_err) {
|
|
115
120
|
const err = _err instanceof Error ? _err : new Error(String(_err));
|
|
116
121
|
this.log.write("error", `mcp: reconnect failed: ${err.message}`);
|
|
117
|
-
reconnect(attempt + 1).catch(() => { });
|
|
122
|
+
reconnect(attempt + 1, done).catch(() => { });
|
|
118
123
|
}
|
|
119
124
|
};
|
|
120
125
|
/* wire stdio server */
|
|
@@ -145,7 +150,7 @@ export default class MCPCommand {
|
|
|
145
150
|
if (match !== true) {
|
|
146
151
|
reconnecting = true;
|
|
147
152
|
this.log.write("warning", "mcp: health check failed — reconnecting");
|
|
148
|
-
reconnect(
|
|
153
|
+
reconnect(0, () => { reconnecting = false; }).catch(() => { });
|
|
149
154
|
}
|
|
150
155
|
}
|
|
151
156
|
catch { /* ignore probe errors */ }
|
|
@@ -0,0 +1,89 @@
|
|
|
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 { isScalar } from "yaml";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { Config, configSchema, parseScope } from "./ase-config.js";
|
|
9
|
+
/* reusable functionality: ASE agent persona style get/set */
|
|
10
|
+
export class Persona {
|
|
11
|
+
/* allowed persona style values */
|
|
12
|
+
static styles = ["writer", "engineer", "telegrapher", "caveman"];
|
|
13
|
+
/* get the effective persona style for an optional session;
|
|
14
|
+
returns the default "engineer" if nothing is configured */
|
|
15
|
+
static get(log, session) {
|
|
16
|
+
const scope = session !== undefined ?
|
|
17
|
+
parseScope(`session:${session}`) :
|
|
18
|
+
parseScope(undefined);
|
|
19
|
+
const cfg = new Config("config", configSchema, log, scope);
|
|
20
|
+
cfg.read();
|
|
21
|
+
const val = cfg.get("agent.persona");
|
|
22
|
+
if (val === undefined)
|
|
23
|
+
return "engineer";
|
|
24
|
+
return String(isScalar(val) ? val.value : val);
|
|
25
|
+
}
|
|
26
|
+
/* set the persona style on the strongest scope of an optional session */
|
|
27
|
+
static set(log, style, session) {
|
|
28
|
+
const scope = session !== undefined ?
|
|
29
|
+
parseScope(`session:${session}`) :
|
|
30
|
+
parseScope(undefined);
|
|
31
|
+
const cfg = new Config("config", configSchema, log, scope);
|
|
32
|
+
cfg.lock(() => {
|
|
33
|
+
cfg.read();
|
|
34
|
+
cfg.set("agent.persona", style);
|
|
35
|
+
cfg.write();
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/* MCP registration entry point for persona tools */
|
|
40
|
+
export default class PersonaMCP {
|
|
41
|
+
log;
|
|
42
|
+
constructor(log) {
|
|
43
|
+
this.log = log;
|
|
44
|
+
}
|
|
45
|
+
register(mcp) {
|
|
46
|
+
mcp.registerTool("persona", {
|
|
47
|
+
title: "ASE persona style get/set",
|
|
48
|
+
description: "Get or set the active ASE agent persona `style`. " +
|
|
49
|
+
"If `style` is provided, it sets the persona style, " +
|
|
50
|
+
"otherwise it returns the current persona `style`. " +
|
|
51
|
+
"If `session` is provided, the operation is scoped to that session, " +
|
|
52
|
+
"otherwise it operates on the strongest/closest scope (user/project cascade). " +
|
|
53
|
+
"Allowed styles: \"writer\" (decorative, eloquent, explaining), " +
|
|
54
|
+
"\"engineer\" (brief, factual, accurate), " +
|
|
55
|
+
"\"telegrapher\" (very brief, factual, abbreviating), " +
|
|
56
|
+
"\"caveman\" (ultra brief, rough, stuttering).",
|
|
57
|
+
inputSchema: {
|
|
58
|
+
style: z.enum(Persona.styles).optional()
|
|
59
|
+
.describe("persona style to set; if omitted, the current persona style is returned"),
|
|
60
|
+
session: z.string().optional()
|
|
61
|
+
.describe("session identifier (allowed characters: A-Z, a-z, 0-9, '-'); " +
|
|
62
|
+
"if omitted, the operation is not scoped to a specific session")
|
|
63
|
+
}
|
|
64
|
+
}, async (args) => {
|
|
65
|
+
try {
|
|
66
|
+
if (args.style !== undefined) {
|
|
67
|
+
Persona.set(this.log, args.style, args.session);
|
|
68
|
+
const where = args.session !== undefined ?
|
|
69
|
+
` for session "${args.session}"` : "";
|
|
70
|
+
const msg = `persona: OK: set agent.persona to "${args.style}"${where}`;
|
|
71
|
+
return {
|
|
72
|
+
content: [{ type: "text", text: msg }]
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const text = Persona.get(this.log, args.session);
|
|
76
|
+
return {
|
|
77
|
+
content: [{ type: "text", text }]
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
82
|
+
return {
|
|
83
|
+
isError: true,
|
|
84
|
+
content: [{ type: "text", text: `persona: ERROR: ${message}` }]
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|