@rse/ase 0.9.43 → 0.9.44

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 (73) hide show
  1. package/dst/ase-artifact.js +28 -24
  2. package/dst/ase-config.js +27 -36
  3. package/dst/ase-diagram.js +15 -35
  4. package/dst/ase-getopt.js +10 -15
  5. package/dst/ase-hook.js +29 -27
  6. package/dst/ase-kv.js +24 -29
  7. package/dst/ase-log.js +10 -3
  8. package/dst/ase-markdown.js +135 -102
  9. package/dst/ase-mcp.js +3 -10
  10. package/dst/ase-meta.js +6 -4
  11. package/dst/ase-persona.js +7 -11
  12. package/dst/ase-service.js +28 -28
  13. package/dst/ase-setup.js +15 -28
  14. package/dst/ase-skills.js +2 -2
  15. package/dst/ase-statusline.js +26 -34
  16. package/dst/ase-task.js +60 -63
  17. package/dst/ase-version.js +2 -1
  18. package/dst/ase.js +7 -11
  19. package/package.json +12 -10
  20. package/plugin/.claude-plugin/plugin.json +1 -1
  21. package/plugin/.codex-plugin/plugin.json +1 -1
  22. package/plugin/.github/plugin/plugin.json +2 -2
  23. package/plugin/agents/ase-code-lint.md +4 -4
  24. package/plugin/agents/ase-docs-proofread.md +1 -1
  25. package/plugin/agents/ase-meta-chat.md +6 -6
  26. package/plugin/agents/ase-meta-diagram.md +1 -1
  27. package/plugin/agents/ase-meta-search.md +1 -1
  28. package/plugin/etc/eslint.mjs +0 -2
  29. package/plugin/meta/ase-constitution.md +1 -1
  30. package/plugin/meta/ase-control.md +1 -1
  31. package/plugin/meta/ase-dialog.md +2 -2
  32. package/plugin/meta/ase-format-arch.md +8 -8
  33. package/plugin/meta/ase-format-task.md +8 -10
  34. package/plugin/meta/ase-getopt.md +1 -1
  35. package/plugin/meta/ase-skill.md +1 -1
  36. package/plugin/package.json +8 -8
  37. package/plugin/skills/ase-arch-analyze/help.md +2 -2
  38. package/plugin/skills/ase-arch-discover/SKILL.md +2 -2
  39. package/plugin/skills/ase-arch-discover/help.md +3 -2
  40. package/plugin/skills/ase-code-analyze/SKILL.md +0 -1
  41. package/plugin/skills/ase-code-craft/SKILL.md +4 -4
  42. package/plugin/skills/ase-code-lint/SKILL.md +12 -10
  43. package/plugin/skills/ase-code-refactor/SKILL.md +6 -6
  44. package/plugin/skills/ase-code-refactor/help.md +1 -1
  45. package/plugin/skills/ase-code-resolve/SKILL.md +4 -4
  46. package/plugin/skills/ase-code-resolve/help.md +1 -1
  47. package/plugin/skills/ase-docs-distill/help.md +6 -4
  48. package/plugin/skills/ase-docs-proofread/SKILL.md +5 -5
  49. package/plugin/skills/ase-meta-brainstorm/help.md +1 -1
  50. package/plugin/skills/ase-meta-changelog/SKILL.md +4 -3
  51. package/plugin/skills/ase-meta-commit/SKILL.md +3 -1
  52. package/plugin/skills/ase-meta-compat/SKILL.md +11 -2
  53. package/plugin/skills/ase-meta-diaboli/SKILL.md +4 -4
  54. package/plugin/skills/ase-meta-diff/SKILL.md +10 -10
  55. package/plugin/skills/ase-meta-persona/SKILL.md +1 -1
  56. package/plugin/skills/ase-meta-quorum/SKILL.md +10 -8
  57. package/plugin/skills/ase-meta-search/help.md +4 -2
  58. package/plugin/skills/ase-meta-steelman/SKILL.md +2 -2
  59. package/plugin/skills/ase-meta-steelman/help.md +5 -3
  60. package/plugin/skills/ase-meta-why/SKILL.md +1 -1
  61. package/plugin/skills/ase-meta-why/help.md +3 -1
  62. package/plugin/skills/ase-sync-export/SKILL.md +13 -10
  63. package/plugin/skills/ase-sync-import/help.md +2 -2
  64. package/plugin/skills/ase-sync-reconcile/SKILL.md +1 -1
  65. package/plugin/skills/ase-sync-reconcile/help.md +1 -1
  66. package/plugin/skills/ase-task-condense/SKILL.md +6 -7
  67. package/plugin/skills/ase-task-delete/SKILL.md +1 -1
  68. package/plugin/skills/ase-task-edit/SKILL.md +2 -2
  69. package/plugin/skills/ase-task-grill/SKILL.md +1 -1
  70. package/plugin/skills/ase-task-implement/help.md +2 -2
  71. package/plugin/skills/ase-task-preflight/SKILL.md +1 -1
  72. package/plugin/skills/ase-task-reboot/SKILL.md +13 -9
  73. package/plugin/skills/ase-task-reboot/help.md +2 -1
package/dst/ase-kv.js CHANGED
@@ -95,7 +95,7 @@ export class KVMCP {
95
95
  }
96
96
  catch (err) {
97
97
  const message = err instanceof Error ? err.message : String(err);
98
- return { isError: true, content: [{ type: "text", text: `kv_get: ERROR: ${message}` }] };
98
+ return { isError: true, content: [{ type: "text", text: `ERROR: ${message}` }] };
99
99
  }
100
100
  });
101
101
  /* key/value set */
@@ -107,17 +107,17 @@ export class KVMCP {
107
107
  inputSchema: {
108
108
  key: z.string()
109
109
  .describe("key identifier (non-empty, no whitespace-only, no control characters, up to 1024 characters)"),
110
- val: z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(z.any()), z.record(z.string(), z.any())])
110
+ val: z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(z.unknown()), z.record(z.string(), z.unknown())])
111
111
  .describe("arbitrary JSON-compatible value to store under `key`")
112
112
  }
113
113
  }, async (args) => {
114
114
  try {
115
115
  KV.set(args.key, args.val);
116
- return { content: [{ type: "text", text: `kv_set: OK: stored key "${args.key}"` }] };
116
+ return { content: [{ type: "text", text: `OK: stored key "${args.key}"` }] };
117
117
  }
118
118
  catch (err) {
119
119
  const message = err instanceof Error ? err.message : String(err);
120
- return { isError: true, content: [{ type: "text", text: `kv_set: ERROR: ${message}` }] };
120
+ return { isError: true, content: [{ type: "text", text: `ERROR: ${message}` }] };
121
121
  }
122
122
  });
123
123
  /* key/value clear */
@@ -134,11 +134,11 @@ export class KVMCP {
134
134
  }, async (args) => {
135
135
  try {
136
136
  const n = KV.clear(args.prefix);
137
- return { content: [{ type: "text", text: `kv_clear: OK: removed ${n} key(s)` }] };
137
+ return { content: [{ type: "text", text: `OK: removed ${n} key(s)` }] };
138
138
  }
139
139
  catch (err) {
140
140
  const message = err instanceof Error ? err.message : String(err);
141
- return { isError: true, content: [{ type: "text", text: `kv_clear: ERROR: ${message}` }] };
141
+ return { isError: true, content: [{ type: "text", text: `ERROR: ${message}` }] };
142
142
  }
143
143
  });
144
144
  /* key/value delete */
@@ -154,13 +154,13 @@ export class KVMCP {
154
154
  try {
155
155
  const removed = KV.delete(args.key);
156
156
  const msg = removed ?
157
- `kv_delete: OK: removed key "${args.key}"` :
158
- `kv_delete: WARNING: no key "${args.key}" to remove`;
157
+ `OK: removed key "${args.key}"` :
158
+ `WARNING: no key "${args.key}" to remove`;
159
159
  return { content: [{ type: "text", text: msg }] };
160
160
  }
161
161
  catch (err) {
162
162
  const message = err instanceof Error ? err.message : String(err);
163
- return { isError: true, content: [{ type: "text", text: `kv_delete: ERROR: ${message}` }] };
163
+ return { isError: true, content: [{ type: "text", text: `ERROR: ${message}` }] };
164
164
  }
165
165
  });
166
166
  /* key/value batch */
@@ -174,17 +174,17 @@ export class KVMCP {
174
174
  "first per-command error (remaining commands are skipped); otherwise per-command errors " +
175
175
  "are recorded and execution continues. " +
176
176
  "Returns a single `text` payload containing a JSON array of per-command result strings " +
177
- "in the same format emitted by `kv_clear`/`kv_set`/`kv_get`/`kv_delete`. " +
177
+ "positionally matching the `commands` array. " +
178
178
  "On transactional rollback, prior per-command result strings are rewritten to " +
179
- "`kv_<cmd>: ROLLED-BACK` to truthfully reflect the post-rollback state, and the " +
180
- "final entry remains `kv_batch: ERROR: <message>`.",
179
+ "`ROLLED-BACK` to truthfully reflect the post-rollback state, and the " +
180
+ "final entry remains `ERROR: <message>`.",
181
181
  inputSchema: {
182
182
  commands: z.array(z.object({
183
183
  command: z.enum(["clear", "set", "get", "delete"])
184
184
  .describe("the KV sub-command to execute"),
185
185
  key: z.string().optional()
186
186
  .describe("key identifier (required for `set`/`get`/`delete`)"),
187
- val: z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(z.any()), z.record(z.string(), z.any())]).optional()
187
+ val: z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(z.unknown()), z.record(z.string(), z.unknown())]).optional()
188
188
  .describe("value to store (required for `set`)"),
189
189
  prefix: z.string().optional()
190
190
  .describe("if given for `clear`, only remove keys starting with this prefix (otherwise remove all keys)")
@@ -195,44 +195,39 @@ export class KVMCP {
195
195
  }
196
196
  }, async (args) => {
197
197
  const results = [];
198
- const cmdNames = [];
199
198
  const tx = args.transactional === true;
200
199
  const snapshot = tx ? KV.snapshot() : null;
201
200
  for (const c of args.commands) {
202
201
  try {
203
202
  if (c.command === "clear") {
204
203
  const n = KV.clear(c.prefix);
205
- results.push(`kv_clear: OK: removed ${n} key(s)`);
206
- cmdNames.push("clear");
204
+ results.push(`OK: removed ${n} key(s)`);
207
205
  }
208
206
  else if (c.command === "set") {
209
207
  if (c.key === undefined)
210
- throw new Error("kv_set: missing `key`");
208
+ throw new Error("missing `key`");
211
209
  if (c.val === undefined)
212
- throw new Error("kv_set: missing `val`");
210
+ throw new Error("missing `val`");
213
211
  KV.set(c.key, c.val);
214
- results.push(`kv_set: OK: stored key "${c.key}"`);
215
- cmdNames.push("set");
212
+ results.push(`OK: stored key "${c.key}"`);
216
213
  }
217
214
  else if (c.command === "get") {
218
215
  if (c.key === undefined)
219
- throw new Error("kv_get: missing `key`");
216
+ throw new Error("missing `key`");
220
217
  if (!KV.has(c.key))
221
218
  results.push("");
222
219
  else {
223
220
  const val = KV.get(c.key);
224
221
  results.push(val === undefined ? "" : JSON.stringify(val));
225
222
  }
226
- cmdNames.push("get");
227
223
  }
228
224
  else if (c.command === "delete") {
229
225
  if (c.key === undefined)
230
- throw new Error("kv_delete: missing `key`");
226
+ throw new Error("missing `key`");
231
227
  const removed = KV.delete(c.key);
232
228
  results.push(removed ?
233
- `kv_delete: OK: removed key "${c.key}"` :
234
- `kv_delete: WARNING: no key "${c.key}" to remove`);
235
- cmdNames.push("delete");
229
+ `OK: removed key "${c.key}"` :
230
+ `WARNING: no key "${c.key}" to remove`);
236
231
  }
237
232
  }
238
233
  catch (err) {
@@ -241,11 +236,11 @@ export class KVMCP {
241
236
  if (snapshot !== null)
242
237
  KV.restore(snapshot);
243
238
  for (let i = 0; i < results.length; i++)
244
- results[i] = `kv_${cmdNames[i]}: ROLLED-BACK`;
245
- results.push(`kv_batch: ERROR: ${message}`);
239
+ results[i] = "ROLLED-BACK";
240
+ results.push(`ERROR: ${message}`);
246
241
  return { isError: true, content: [{ type: "text", text: JSON.stringify(results) }] };
247
242
  }
248
- results.push(`kv_${c.command}: ERROR: ${message}`);
243
+ results.push(`ERROR: ${message}`);
249
244
  }
250
245
  }
251
246
  return { content: [{ type: "text", text: JSON.stringify(results) }] };
package/dst/ase-log.js CHANGED
@@ -30,7 +30,14 @@ export default class Log {
30
30
  throw new RangeError(`invalid log level "${this._logLevel}" (expected one of: ${levels.map((l) => l.name).join(", ")})`);
31
31
  this.logLevelIdx = idx;
32
32
  if (this._logFile !== "-")
33
- this.stream = fs.createWriteStream(this._logFile, { flags: "a", encoding: "utf8" });
33
+ this.stream = this.openStream(this._logFile);
34
+ }
35
+ openStream(file) {
36
+ const stream = fs.createWriteStream(file, { flags: "a", encoding: "utf8" });
37
+ stream.on("error", (err) => {
38
+ process.stderr.write(`${this._program}: ERROR: cannot write log file "${file}": ${err.message}\n`);
39
+ });
40
+ return stream;
34
41
  }
35
42
  logLevel(level) {
36
43
  const idx = levels.findIndex((l) => l.name === level);
@@ -48,14 +55,14 @@ export default class Log {
48
55
  this.stream = null;
49
56
  }
50
57
  if (file !== "-")
51
- this.stream = fs.createWriteStream(file, { flags: "a", encoding: "utf8" });
58
+ this.stream = this.openStream(file);
52
59
  }
53
60
  write(level, msg) {
54
61
  const idx = levels.findIndex((l) => l.name === level);
55
62
  if (idx !== -1 && idx <= this.logLevelIdx) {
56
63
  const timestamp = DateTime.now().toFormat("yyyy-LL-dd HH:mm:ss.SSS");
57
64
  let line = `${this._program}: [${timestamp}]: `;
58
- if (this._logFile === "-" && process.stdout.isTTY)
65
+ if (this._logFile === "-" && process.stderr.isTTY)
59
66
  line += `${levels[idx].style("[" + levels[idx].name.toUpperCase() + "]")}`;
60
67
  else
61
68
  line += `[${levels[idx].name.toUpperCase()}]`;
@@ -37,22 +37,22 @@ export class Markdown {
37
37
  for (let li = 0; li < lines.length; li++) {
38
38
  const line = lines[li];
39
39
  const nl = li < lines.length - 1 ? "\n" : "";
40
- const m = line.match(/^(\s*)(`{3,}|~{3,})/);
40
+ const m = line.match(/^\s*(`{3,}|~{3,})/);
41
41
  if (!inFence && m) {
42
42
  /* a fence-opening line: flush the pending non-fenced buffer,
43
43
  enter fenced mode, and emit the opener verbatim */
44
44
  flush();
45
45
  inFence = true;
46
- fenceCh = m[2][0];
47
- fenceLen = m[2].length;
46
+ fenceCh = m[1][0];
47
+ fenceLen = m[1].length;
48
48
  result += line + nl;
49
49
  }
50
50
  else if (inFence) {
51
51
  /* inside a fenced block: emit verbatim and, on a matching
52
52
  closing fence line, leave fenced mode */
53
53
  result += line + nl;
54
- const c = line.match(/^(\s*)(`{3,}|~{3,})\s*$/);
55
- if (c && c[2][0] === fenceCh && c[2].length >= fenceLen)
54
+ const c = line.match(/^\s*(`{3,}|~{3,})\s*$/);
55
+ if (c && c[1][0] === fenceCh && c[1].length >= fenceLen)
56
56
  inFence = false;
57
57
  }
58
58
  else
@@ -62,100 +62,104 @@ export class Markdown {
62
62
  flush();
63
63
  return result;
64
64
  }
65
- /* apply the inline-span and bullet-marker rewriting passes to a chunk of
66
- non-fenced Markdown text (see prepare() for fenced-block handling) */
67
- static rewrite(text) {
68
- /* PASS 1: rewrite inline code spans that carry backslash-escaped
69
- backticks (`\``) into CommonMark-correct spans. */
70
- {
71
- let pre = "";
72
- let j = 0;
73
- while (j < text.length) {
74
- const ch = text[j];
75
- if (ch !== "`") {
76
- /* literal backslash-escaped backtick *outside* any span
77
- is left verbatim for later scanning */
78
- pre += ch;
79
- j++;
65
+ /* measure the length of the backtick run starting at offset pos */
66
+ static runLength(text, pos) {
67
+ let n = 0;
68
+ while (pos + n < text.length && text[pos + n] === "`")
69
+ n++;
70
+ return n;
71
+ }
72
+ /* PASS 1 of rewrite(): rewrite inline code spans that carry
73
+ backslash-escaped backticks (`\``) into CommonMark-correct spans */
74
+ static rewriteEscapedSpans(text) {
75
+ let pre = "";
76
+ let j = 0;
77
+ while (j < text.length) {
78
+ const ch = text[j];
79
+ if (ch !== "`") {
80
+ /* literal backslash-escaped backtick *outside* any span
81
+ is left verbatim for later scanning */
82
+ pre += ch;
83
+ j++;
84
+ continue;
85
+ }
86
+ /* measure the full opening backtick run: a code-span
87
+ delimiter is a *run* of backticks and a span opened by a
88
+ run of N backticks is closed only by a run of exactly N
89
+ backticks, so the opening-run length determines what we
90
+ scan for as the closing delimiter */
91
+ const open = Markdown.runLength(text, j);
92
+ /* scan the opening backtick-run span, capturing its raw
93
+ inner content up to the matching unescaped closing run of
94
+ exactly `open` backticks */
95
+ let inner = "";
96
+ let k = j + open;
97
+ let closed = false;
98
+ let escaped = false;
99
+ while (k < text.length) {
100
+ const c = text[k];
101
+ if (c === "\\" && k + 1 < text.length && text[k + 1] === "`") {
102
+ inner += "\\`";
103
+ escaped = true;
104
+ k += 2;
80
105
  continue;
81
106
  }
82
- /* measure the full opening backtick run: a code-span
83
- delimiter is a *run* of backticks and a span opened by a
84
- run of N backticks is closed only by a run of exactly N
85
- backticks, so the opening-run length determines what we
86
- scan for as the closing delimiter */
87
- let open = 0;
88
- while (j + open < text.length && text[j + open] === "`")
89
- open++;
90
- /* scan the opening backtick-run span, capturing its raw
91
- inner content up to the matching unescaped closing run of
92
- exactly `open` backticks */
93
- let inner = "";
94
- let k = j + open;
95
- let closed = false;
96
- let escaped = false;
97
- while (k < text.length) {
98
- const c = text[k];
99
- if (c === "\\" && k + 1 < text.length && text[k + 1] === "`") {
100
- inner += "\\`";
101
- escaped = true;
102
- k += 2;
103
- continue;
107
+ if (c === "`") {
108
+ /* measure this backtick run and treat it as the
109
+ closing delimiter only if it matches the opening
110
+ run length exactly; a shorter or longer run is
111
+ literal content of the span */
112
+ const runLen = Markdown.runLength(text, k);
113
+ if (runLen === open) {
114
+ closed = true;
115
+ break;
104
116
  }
105
- if (c === "`") {
106
- /* measure this backtick run and treat it as the
107
- closing delimiter only if it matches the opening
108
- run length exactly; a shorter or longer run is
109
- literal content of the span */
110
- let runLen = 0;
111
- while (k + runLen < text.length && text[k + runLen] === "`")
112
- runLen++;
113
- if (runLen === open) {
114
- closed = true;
115
- break;
116
- }
117
- inner += "`".repeat(runLen);
118
- k += runLen;
119
- continue;
120
- }
121
- inner += c;
122
- k++;
123
- }
124
- if (!closed || !escaped) {
125
- /* not an escaped-backtick span: emit the opening run
126
- verbatim and continue scanning from just after it */
127
- pre += "`".repeat(open);
128
- j += open;
117
+ inner += "`".repeat(runLen);
118
+ k += runLen;
129
119
  continue;
130
120
  }
131
- /* un-escape inner `\`` into literal backticks, then choose a
132
- fence longer than the longest internal backtick run */
133
- const content = inner.replace(/\\`/g, "`");
134
- let maxRun = 0;
135
- let run = 0;
136
- for (const c of content) {
137
- if (c === "`") {
138
- run++;
139
- if (run > maxRun)
140
- maxRun = run;
141
- }
142
- else
143
- run = 0;
121
+ if (c === "\n" && /^[ \t]*\r?(?:\n|$)/.test(text.slice(k + 1)))
122
+ /* a code span never crosses a paragraph break */
123
+ break;
124
+ inner += c;
125
+ k++;
126
+ }
127
+ if (!closed || !escaped) {
128
+ /* not an escaped-backtick span: emit the opening run
129
+ verbatim and continue scanning from just after it */
130
+ pre += "`".repeat(open);
131
+ j += open;
132
+ continue;
133
+ }
134
+ /* un-escape inner `\`` into literal backticks, then choose a
135
+ fence longer than the longest internal backtick run */
136
+ const content = inner.replace(/\\`/g, "`");
137
+ let maxRun = 0;
138
+ let run = 0;
139
+ for (const c of content) {
140
+ if (c === "`") {
141
+ run++;
142
+ if (run > maxRun)
143
+ maxRun = run;
144
144
  }
145
- const fence = "`".repeat(maxRun + 1);
146
- const pad = (content.startsWith("`") || content.endsWith("`")) ? " " : "";
147
- pre += `${fence}${pad}${content}${pad}${fence}`;
148
- j = k + open;
145
+ else
146
+ run = 0;
149
147
  }
150
- text = pre;
148
+ const fence = "`".repeat(maxRun + 1);
149
+ const pad = (content.startsWith("`") || content.endsWith("`")) ? " " : "";
150
+ pre += `${fence}${pad}${content}${pad}${fence}`;
151
+ j = k + open;
151
152
  }
152
- /* PASS 2: split multi-line inline code spans by scanning the
153
- entire text character-by-character while tracking whether we
154
- are currently inside an active backtick (U+0060) span; on
155
- every "<newline><whitespaces>" sequence encountered while
156
- inside a span, close the span before the break and re-open
157
- it after the indentation, so each physical line becomes its
158
- own single-line span */
153
+ return pre;
154
+ }
155
+ /* PASS 2 of rewrite(): split multi-line inline code spans by scanning
156
+ the entire text character-by-character while tracking whether we
157
+ are currently inside an active backtick (U+0060) span; on
158
+ every "<newline><whitespaces>" sequence encountered while
159
+ inside a span, close the span before the break and re-open
160
+ it after the indentation, so each physical line becomes its
161
+ own single-line span */
162
+ static splitMultiLineSpans(text) {
159
163
  let out = "";
160
164
  let fence = 0;
161
165
  let i = 0;
@@ -165,14 +169,31 @@ export class Markdown {
165
169
  /* measure the full backtick run (a code-span delimiter is a
166
170
  *run* of backticks; a span opened by N backticks is closed
167
171
  only by a run of exactly N backticks) */
168
- let n = 0;
169
- while (i < text.length && text[i] === "`") {
170
- n++;
171
- i++;
172
+ const n = Markdown.runLength(text, i);
173
+ i += n;
174
+ if (fence === 0) {
175
+ /* open a span of fence width n, but only if a matching
176
+ closing run of exactly n backticks exists ahead (an
177
+ unmatched run is literal text per CommonMark and must
178
+ not trigger the line-splitting below) */
179
+ let p = i;
180
+ let closes = false;
181
+ while (p < text.length && !closes) {
182
+ if (text[p] === "`") {
183
+ const r = Markdown.runLength(text, p);
184
+ if (r === n)
185
+ closes = true;
186
+ p += r;
187
+ }
188
+ else if (text[p] === "\n" && /^[ \t]*\r?(?:\n|$)/.test(text.slice(p + 1)))
189
+ /* a code span never crosses a paragraph break */
190
+ break;
191
+ else
192
+ p++;
193
+ }
194
+ if (closes)
195
+ fence = n;
172
196
  }
173
- if (fence === 0)
174
- /* open a span of fence width n */
175
- fence = n;
176
197
  else if (n === fence)
177
198
  /* close the active span */
178
199
  fence = 0;
@@ -203,23 +224,35 @@ export class Markdown {
203
224
  i++;
204
225
  }
205
226
  /* close span before the break, re-open after indentation,
206
- preserving the active fence width on both sides */
227
+ preserving the active fence width on both sides and padding
228
+ with a space wherever an inserted delimiter run would merge
229
+ with an adjacent literal backtick of the span content */
207
230
  const bars = "`".repeat(fence);
208
- out += `${bars}${nl}${ws}${bars}`;
231
+ const padC = out.endsWith("`") ? " " : "";
232
+ const padO = (i < text.length && text[i] === "`") ? " " : "";
233
+ out += `${padC}${bars}${nl}${ws}${bars}${padO}`;
209
234
  continue;
210
235
  }
211
236
  out += ch;
212
237
  i++;
213
238
  }
239
+ return out;
240
+ }
241
+ /* apply the inline-span and bullet-marker rewriting passes to a chunk of
242
+ non-fenced Markdown text (see prepare() for fenced-block handling) */
243
+ static rewrite(text) {
244
+ /* PASS 1: rewrite escaped-backtick inline code spans */
245
+ text = Markdown.rewriteEscapedSpans(text);
246
+ /* PASS 2: split multi-line inline code spans into per-line spans */
247
+ const out = Markdown.splitMultiLineSpans(text);
214
248
  /* PASS 3: replace the leading "-"/"*" marker of every unordered
215
249
  bullet paragraph with "◯", preserving the leading indentation
216
250
  and the following whitespace; operate line-by-line so only the
217
251
  actual list markers (at a line start) are affected */
218
- out = out
252
+ return out
219
253
  .split("\n")
220
254
  .map((line) => line.replace(/^(\s*)[-*]([ \t]+)/, "$1◯$2"))
221
255
  .join("\n");
222
- return out;
223
256
  }
224
257
  }
225
258
  /* MCP registration entry point */
package/dst/ase-mcp.js CHANGED
@@ -3,13 +3,11 @@
3
3
  ** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
4
4
  ** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
5
5
  */
6
- import path from "node:path";
7
6
  import { fileURLToPath } from "node:url";
8
7
  import { execa } from "execa";
9
8
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
10
9
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
11
- import { Config, configSchema } from "./ase-config.js";
12
- import { SERVICE_HOST as HOST, serviceSchema, probe } from "./ase-service.js";
10
+ import { SERVICE_HOST as HOST, probe, loadServiceContext } from "./ase-service.js";
13
11
  /* CLI command "ase mcp" */
14
12
  export default class MCPCommand {
15
13
  log;
@@ -18,13 +16,7 @@ export default class MCPCommand {
18
16
  }
19
17
  /* load service identity context */
20
18
  loadContext() {
21
- const cfg = new Config("config", configSchema, this.log);
22
- cfg.read();
23
- const svc = new Config("service", serviceSchema, this.log);
24
- svc.read();
25
- const projectId = cfg.get("project.id") ?? path.basename(process.cwd());
26
- const port = svc.get("port") ?? null;
27
- return { projectId, port };
19
+ return loadServiceContext(this.log);
28
20
  }
29
21
  /* run "ase service start" and wait for the service to come up */
30
22
  async ensureService() {
@@ -116,6 +108,7 @@ export default class MCPCommand {
116
108
  try {
117
109
  const ctx = await this.ensureService();
118
110
  port = ctx.port;
111
+ projectId = ctx.projectId;
119
112
  const stale = client;
120
113
  client = null;
121
114
  if (stale !== null) {
package/dst/ase-meta.js CHANGED
@@ -39,8 +39,9 @@ export class Meta {
39
39
  try {
40
40
  return fs.readFileSync(abs, "utf8");
41
41
  }
42
- catch (_e) {
43
- throw new Error(`meta: failed to read file: ${abs}`);
42
+ catch (err) {
43
+ const message = err instanceof Error ? err.message : String(err);
44
+ throw new Error(`meta: failed to read file: ${abs} (${message})`);
44
45
  }
45
46
  }
46
47
  }
@@ -58,8 +59,9 @@ export default class MetaCommand {
58
59
  .argument("<name...>", "meta file name(s) (\"ase-\" prefix and \".md\" extension optional)")
59
60
  .action(async (names) => {
60
61
  this.log.write("debug", `meta: reading ${names.length} file(s)`);
61
- for (const name of names)
62
- await writeStdout(Meta.read(name));
62
+ const texts = names.map((name) => Meta.read(name));
63
+ for (const text of texts)
64
+ await writeStdout(text);
63
65
  });
64
66
  }
65
67
  }
@@ -5,17 +5,15 @@
5
5
  */
6
6
  import { isScalar } from "yaml";
7
7
  import { z } from "zod";
8
- import { Config, configSchema, parseScope } from "./ase-config.js";
8
+ import { Config, configSchema, parseScope, agentClassification } from "./ase-config.js";
9
9
  /* reusable functionality: ASE agent persona style get/set */
10
10
  export class Persona {
11
11
  /* allowed persona style values */
12
- static styles = ["writer", "engineer", "journalist", "telegrapher", "caveman"];
12
+ static styles = agentClassification.persona;
13
13
  /* get the effective persona style for an optional session;
14
14
  returns the default "engineer" if nothing is configured */
15
15
  static get(log, session) {
16
- const scope = session !== undefined ?
17
- parseScope(`session:${session}`) :
18
- parseScope(undefined);
16
+ const scope = parseScope(session !== undefined ? `session:${session}` : undefined);
19
17
  const cfg = new Config("config", configSchema, log, scope);
20
18
  cfg.read();
21
19
  const val = cfg.get("agent.persona");
@@ -28,9 +26,7 @@ export class Persona {
28
26
  }
29
27
  /* set the persona style on the strongest scope of an optional session */
30
28
  static set(log, style, session) {
31
- const scope = session !== undefined ?
32
- parseScope(`session:${session}`) :
33
- parseScope(undefined);
29
+ const scope = parseScope(session !== undefined ? `session:${session}` : undefined);
34
30
  const cfg = new Config("config", configSchema, log, scope);
35
31
  cfg.lock(() => {
36
32
  cfg.read();
@@ -61,7 +57,7 @@ export default class PersonaMCP {
61
57
  inputSchema: {
62
58
  style: z.enum(Persona.styles).optional()
63
59
  .describe("persona style to set; if omitted, the current persona style is returned"),
64
- session: z.string().optional()
60
+ session: z.string().regex(/^[A-Za-z0-9._-]+$/).optional()
65
61
  .describe("session identifier (allowed characters: A-Z, a-z, 0-9, '.', '_', '-'); " +
66
62
  "if omitted, the operation is not scoped to a specific session")
67
63
  }
@@ -71,7 +67,7 @@ export default class PersonaMCP {
71
67
  Persona.set(this.log, args.style, args.session);
72
68
  const where = args.session !== undefined ?
73
69
  ` for session "${args.session}"` : "";
74
- const msg = `persona: OK: set agent.persona to "${args.style}"${where}`;
70
+ const msg = `OK: set agent.persona to "${args.style}"${where}`;
75
71
  return {
76
72
  content: [{ type: "text", text: msg }]
77
73
  };
@@ -85,7 +81,7 @@ export default class PersonaMCP {
85
81
  const message = err instanceof Error ? err.message : String(err);
86
82
  return {
87
83
  isError: true,
88
- content: [{ type: "text", text: `persona: ERROR: ${message}` }]
84
+ content: [{ type: "text", text: `ERROR: ${message}` }]
89
85
  };
90
86
  }
91
87
  });