@rse/ase 0.9.46 → 0.9.48
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-artifact.js +3 -1
- package/dst/ase-compat.js +3 -2
- package/dst/ase-config.js +36 -16
- package/dst/ase-diagram.js +10 -15
- package/dst/ase-getopt.js +66 -60
- package/dst/ase-hook.js +51 -77
- package/dst/ase-kv.js +49 -79
- package/dst/ase-log.js +13 -5
- package/dst/ase-markdown.js +12 -2
- package/dst/ase-mcp.js +21 -4
- package/dst/ase-meta.js +1 -1
- package/dst/ase-persona.js +1 -3
- package/dst/ase-service.js +17 -27
- package/dst/ase-setup.js +29 -73
- package/dst/ase-skills.js +1 -1
- package/dst/ase-statusline.js +28 -42
- package/dst/ase-stdio.js +25 -0
- package/dst/ase-task.js +25 -34
- package/dst/ase.js +7 -3
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/.github/plugin/plugin.json +1 -1
- package/plugin/etc/stx.conf +2 -2
- package/plugin/meta/ase-skill.md +9 -0
- package/plugin/package.json +1 -1
- package/plugin/skills/{ase-meta-intent → ase-help-intent}/SKILL.md +7 -12
- package/plugin/skills/{ase-meta-intent → ase-help-intent}/help.md +8 -7
- package/plugin/skills/ase-help-skill/SKILL.md +203 -0
- package/plugin/skills/ase-help-skill/catalog.md +60 -0
- package/plugin/skills/ase-help-skill/help.md +83 -0
- package/plugin/skills/ase-task-condense/help.md +3 -3
- package/plugin/skills/ase-task-reboot/help.md +3 -3
- package/plugin/skills/ase-task-view/help.md +3 -3
package/dst/ase-kv.js
CHANGED
|
@@ -30,10 +30,10 @@ export class KV {
|
|
|
30
30
|
KV.validateKey(key);
|
|
31
31
|
return KV.store.has(key);
|
|
32
32
|
}
|
|
33
|
-
/* get a value by key; returns undefined if no value is stored */
|
|
33
|
+
/* get a deep copy of a value by key; returns undefined if no value is stored */
|
|
34
34
|
static get(key) {
|
|
35
35
|
KV.validateKey(key);
|
|
36
|
-
return KV.store.get(key);
|
|
36
|
+
return structuredClone(KV.store.get(key));
|
|
37
37
|
}
|
|
38
38
|
/* set a value under the given key; overwrites any existing value */
|
|
39
39
|
static set(key, val) {
|
|
@@ -72,7 +72,48 @@ export class KV {
|
|
|
72
72
|
for (const [k, v] of snap)
|
|
73
73
|
KV.store.set(k, v);
|
|
74
74
|
}
|
|
75
|
+
/* execute a single key/value command and return its result text */
|
|
76
|
+
static execute(c) {
|
|
77
|
+
if (c.command === "clear")
|
|
78
|
+
return `OK: removed ${KV.clear(c.prefix)} key(s)`;
|
|
79
|
+
else if (c.command === "set") {
|
|
80
|
+
if (c.key === undefined)
|
|
81
|
+
throw new Error("missing `key`");
|
|
82
|
+
if (c.val === undefined)
|
|
83
|
+
throw new Error("missing `val`");
|
|
84
|
+
KV.set(c.key, c.val);
|
|
85
|
+
return `OK: stored key "${c.key}"`;
|
|
86
|
+
}
|
|
87
|
+
else if (c.command === "get") {
|
|
88
|
+
if (c.key === undefined)
|
|
89
|
+
throw new Error("missing `key`");
|
|
90
|
+
if (!KV.has(c.key))
|
|
91
|
+
return "";
|
|
92
|
+
const val = KV.get(c.key);
|
|
93
|
+
return val === undefined ? "" : JSON.stringify(val);
|
|
94
|
+
}
|
|
95
|
+
else if (c.command === "delete") {
|
|
96
|
+
if (c.key === undefined)
|
|
97
|
+
throw new Error("missing `key`");
|
|
98
|
+
return KV.delete(c.key) ?
|
|
99
|
+
`OK: removed key "${c.key}"` :
|
|
100
|
+
`WARNING: no key "${c.key}" to remove`;
|
|
101
|
+
}
|
|
102
|
+
else
|
|
103
|
+
throw new Error(`invalid command "${String(c.command)}"`);
|
|
104
|
+
}
|
|
75
105
|
}
|
|
106
|
+
/* extract the message of a caught error */
|
|
107
|
+
const errorMessage = (err) => err instanceof Error ? err.message : String(err);
|
|
108
|
+
/* execute a single key/value command and render it as an MCP tool result */
|
|
109
|
+
const mcpToolExec = (c) => {
|
|
110
|
+
try {
|
|
111
|
+
return { content: [{ type: "text", text: KV.execute(c) }] };
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
return { isError: true, content: [{ type: "text", text: `ERROR: ${errorMessage(err)}` }] };
|
|
115
|
+
}
|
|
116
|
+
};
|
|
76
117
|
/* MCP registration entry point for in-memory key/value tools */
|
|
77
118
|
export class KVMCP {
|
|
78
119
|
register(mcp) {
|
|
@@ -85,19 +126,7 @@ export class KVMCP {
|
|
|
85
126
|
key: z.string()
|
|
86
127
|
.describe("key identifier (non-empty, no whitespace-only, no control characters, up to 1024 characters)")
|
|
87
128
|
}
|
|
88
|
-
}, async (args) => {
|
|
89
|
-
try {
|
|
90
|
-
if (!KV.has(args.key))
|
|
91
|
-
return { content: [{ type: "text", text: "" }] };
|
|
92
|
-
const val = KV.get(args.key);
|
|
93
|
-
const text = val === undefined ? "" : JSON.stringify(val);
|
|
94
|
-
return { content: [{ type: "text", text }] };
|
|
95
|
-
}
|
|
96
|
-
catch (err) {
|
|
97
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
98
|
-
return { isError: true, content: [{ type: "text", text: `ERROR: ${message}` }] };
|
|
99
|
-
}
|
|
100
|
-
});
|
|
129
|
+
}, async (args) => mcpToolExec({ command: "get", key: args.key }));
|
|
101
130
|
/* key/value set */
|
|
102
131
|
mcp.registerTool("ase_kv_set", {
|
|
103
132
|
title: "ASE key/value set",
|
|
@@ -110,16 +139,7 @@ export class KVMCP {
|
|
|
110
139
|
val: z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(z.unknown()), z.record(z.string(), z.unknown())])
|
|
111
140
|
.describe("arbitrary JSON-compatible value to store under `key`")
|
|
112
141
|
}
|
|
113
|
-
}, async (args) => {
|
|
114
|
-
try {
|
|
115
|
-
KV.set(args.key, args.val);
|
|
116
|
-
return { content: [{ type: "text", text: `OK: stored key "${args.key}"` }] };
|
|
117
|
-
}
|
|
118
|
-
catch (err) {
|
|
119
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
120
|
-
return { isError: true, content: [{ type: "text", text: `ERROR: ${message}` }] };
|
|
121
|
-
}
|
|
122
|
-
});
|
|
142
|
+
}, async (args) => mcpToolExec({ command: "set", key: args.key, val: args.val }));
|
|
123
143
|
/* key/value clear */
|
|
124
144
|
mcp.registerTool("ase_kv_clear", {
|
|
125
145
|
title: "ASE key/value clear",
|
|
@@ -131,16 +151,7 @@ export class KVMCP {
|
|
|
131
151
|
prefix: z.string().optional()
|
|
132
152
|
.describe("if given, only remove keys starting with this prefix (otherwise remove all keys)")
|
|
133
153
|
}
|
|
134
|
-
}, async (args) => {
|
|
135
|
-
try {
|
|
136
|
-
const n = KV.clear(args.prefix);
|
|
137
|
-
return { content: [{ type: "text", text: `OK: removed ${n} key(s)` }] };
|
|
138
|
-
}
|
|
139
|
-
catch (err) {
|
|
140
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
141
|
-
return { isError: true, content: [{ type: "text", text: `ERROR: ${message}` }] };
|
|
142
|
-
}
|
|
143
|
-
});
|
|
154
|
+
}, async (args) => mcpToolExec({ command: "clear", prefix: args.prefix }));
|
|
144
155
|
/* key/value delete */
|
|
145
156
|
mcp.registerTool("ase_kv_delete", {
|
|
146
157
|
title: "ASE key/value delete",
|
|
@@ -150,19 +161,7 @@ export class KVMCP {
|
|
|
150
161
|
key: z.string()
|
|
151
162
|
.describe("key identifier (non-empty, no whitespace-only, no control characters, up to 1024 characters)")
|
|
152
163
|
}
|
|
153
|
-
}, async (args) => {
|
|
154
|
-
try {
|
|
155
|
-
const removed = KV.delete(args.key);
|
|
156
|
-
const msg = removed ?
|
|
157
|
-
`OK: removed key "${args.key}"` :
|
|
158
|
-
`WARNING: no key "${args.key}" to remove`;
|
|
159
|
-
return { content: [{ type: "text", text: msg }] };
|
|
160
|
-
}
|
|
161
|
-
catch (err) {
|
|
162
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
163
|
-
return { isError: true, content: [{ type: "text", text: `ERROR: ${message}` }] };
|
|
164
|
-
}
|
|
165
|
-
});
|
|
164
|
+
}, async (args) => mcpToolExec({ command: "delete", key: args.key }));
|
|
166
165
|
/* key/value batch */
|
|
167
166
|
mcp.registerTool("ase_kv_batch", {
|
|
168
167
|
title: "ASE key/value batch",
|
|
@@ -199,39 +198,10 @@ export class KVMCP {
|
|
|
199
198
|
const snapshot = tx ? KV.snapshot() : null;
|
|
200
199
|
for (const c of args.commands) {
|
|
201
200
|
try {
|
|
202
|
-
|
|
203
|
-
const n = KV.clear(c.prefix);
|
|
204
|
-
results.push(`OK: removed ${n} key(s)`);
|
|
205
|
-
}
|
|
206
|
-
else if (c.command === "set") {
|
|
207
|
-
if (c.key === undefined)
|
|
208
|
-
throw new Error("missing `key`");
|
|
209
|
-
if (c.val === undefined)
|
|
210
|
-
throw new Error("missing `val`");
|
|
211
|
-
KV.set(c.key, c.val);
|
|
212
|
-
results.push(`OK: stored key "${c.key}"`);
|
|
213
|
-
}
|
|
214
|
-
else if (c.command === "get") {
|
|
215
|
-
if (c.key === undefined)
|
|
216
|
-
throw new Error("missing `key`");
|
|
217
|
-
if (!KV.has(c.key))
|
|
218
|
-
results.push("");
|
|
219
|
-
else {
|
|
220
|
-
const val = KV.get(c.key);
|
|
221
|
-
results.push(val === undefined ? "" : JSON.stringify(val));
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
else if (c.command === "delete") {
|
|
225
|
-
if (c.key === undefined)
|
|
226
|
-
throw new Error("missing `key`");
|
|
227
|
-
const removed = KV.delete(c.key);
|
|
228
|
-
results.push(removed ?
|
|
229
|
-
`OK: removed key "${c.key}"` :
|
|
230
|
-
`WARNING: no key "${c.key}" to remove`);
|
|
231
|
-
}
|
|
201
|
+
results.push(KV.execute(c));
|
|
232
202
|
}
|
|
233
203
|
catch (err) {
|
|
234
|
-
const message =
|
|
204
|
+
const message = errorMessage(err);
|
|
235
205
|
if (tx) {
|
|
236
206
|
if (snapshot !== null)
|
|
237
207
|
KV.restore(snapshot);
|
package/dst/ase-log.js
CHANGED
|
@@ -24,11 +24,7 @@ export default class Log {
|
|
|
24
24
|
this._logFile = _logFile;
|
|
25
25
|
}
|
|
26
26
|
async init() {
|
|
27
|
-
|
|
28
|
-
const idx = levels.findIndex((l) => l.name === this._logLevel);
|
|
29
|
-
if (idx === -1)
|
|
30
|
-
throw new RangeError(`invalid log level "${this._logLevel}" (expected one of: ${levels.map((l) => l.name).join(", ")})`);
|
|
31
|
-
this.logLevelIdx = idx;
|
|
27
|
+
this.logLevel(this._logLevel);
|
|
32
28
|
if (this._logFile !== "-")
|
|
33
29
|
this.stream = this.openStream(this._logFile);
|
|
34
30
|
}
|
|
@@ -73,4 +69,16 @@ export default class Log {
|
|
|
73
69
|
this.stream.write(line);
|
|
74
70
|
}
|
|
75
71
|
}
|
|
72
|
+
/* close the log file stream, flushing all pending writes */
|
|
73
|
+
async close() {
|
|
74
|
+
if (this.stream === null)
|
|
75
|
+
return;
|
|
76
|
+
const stream = this.stream;
|
|
77
|
+
this.stream = null;
|
|
78
|
+
await new Promise((resolve) => {
|
|
79
|
+
stream.end(() => {
|
|
80
|
+
resolve();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
}
|
|
76
84
|
}
|
package/dst/ase-markdown.js
CHANGED
|
@@ -69,6 +69,16 @@ export class Markdown {
|
|
|
69
69
|
n++;
|
|
70
70
|
return n;
|
|
71
71
|
}
|
|
72
|
+
/* test whether the newline at offset pos starts a paragraph break,
|
|
73
|
+
i.e. whether it is followed by a blank line or the end of the text */
|
|
74
|
+
static isParagraphBreak(text, pos) {
|
|
75
|
+
let n = pos + 1;
|
|
76
|
+
while (n < text.length && (text[n] === " " || text[n] === "\t"))
|
|
77
|
+
n++;
|
|
78
|
+
if (text[n] === "\r")
|
|
79
|
+
n++;
|
|
80
|
+
return n >= text.length || text[n] === "\n";
|
|
81
|
+
}
|
|
72
82
|
/* PASS 1 of rewrite(): rewrite inline code spans that carry
|
|
73
83
|
backslash-escaped backticks (`\``) into CommonMark-correct spans */
|
|
74
84
|
static rewriteEscapedSpans(text) {
|
|
@@ -118,7 +128,7 @@ export class Markdown {
|
|
|
118
128
|
k += runLen;
|
|
119
129
|
continue;
|
|
120
130
|
}
|
|
121
|
-
if (c === "\n" &&
|
|
131
|
+
if (c === "\n" && Markdown.isParagraphBreak(text, k))
|
|
122
132
|
/* a code span never crosses a paragraph break */
|
|
123
133
|
break;
|
|
124
134
|
inner += c;
|
|
@@ -185,7 +195,7 @@ export class Markdown {
|
|
|
185
195
|
closes = true;
|
|
186
196
|
p += r;
|
|
187
197
|
}
|
|
188
|
-
else if (text[p] === "\n" &&
|
|
198
|
+
else if (text[p] === "\n" && Markdown.isParagraphBreak(text, p))
|
|
189
199
|
/* a code span never crosses a paragraph break */
|
|
190
200
|
break;
|
|
191
201
|
else
|
package/dst/ase-mcp.js
CHANGED
|
@@ -23,7 +23,7 @@ export default class MCPCommand {
|
|
|
23
23
|
let ctx = this.loadContext();
|
|
24
24
|
/* fast path: already running */
|
|
25
25
|
if (ctx.port !== null) {
|
|
26
|
-
const match = await probe(ctx.port, ctx.projectId);
|
|
26
|
+
const match = await probe(ctx.port, ctx.projectId).catch(() => null);
|
|
27
27
|
if (match === true)
|
|
28
28
|
return { projectId: ctx.projectId, port: ctx.port };
|
|
29
29
|
}
|
|
@@ -94,8 +94,11 @@ export default class MCPCommand {
|
|
|
94
94
|
return;
|
|
95
95
|
triggerReconnect("http connection lost");
|
|
96
96
|
};
|
|
97
|
+
/* activate the connection and flush buffered messages */
|
|
97
98
|
await next.start();
|
|
98
99
|
client = next;
|
|
100
|
+
for (const msg of pending.splice(0, pending.length))
|
|
101
|
+
sendToClient(msg);
|
|
99
102
|
};
|
|
100
103
|
/* reconnect loop: restart service if needed, then reconnect client */
|
|
101
104
|
const reconnect = async (attempt = 0) => {
|
|
@@ -132,12 +135,26 @@ export default class MCPCommand {
|
|
|
132
135
|
this.log.write("warning", `mcp: ${reason} — reconnecting`);
|
|
133
136
|
reconnect(0).catch(() => { });
|
|
134
137
|
};
|
|
135
|
-
/*
|
|
136
|
-
|
|
137
|
-
|
|
138
|
+
/* bounded buffer for messages arriving while no HTTP client is connected */
|
|
139
|
+
const MAX_PENDING = 100;
|
|
140
|
+
const pending = [];
|
|
141
|
+
/* forward a message to the HTTP client, buffering it while the client
|
|
142
|
+
is disconnected so requests survive a reconnect window */
|
|
143
|
+
const sendToClient = (msg) => {
|
|
144
|
+
if (client === null) {
|
|
145
|
+
if (pending.length >= MAX_PENDING) {
|
|
146
|
+
this.log.write("warning", "mcp: pending queue overflow, dropping oldest message");
|
|
147
|
+
pending.shift();
|
|
148
|
+
}
|
|
149
|
+
pending.push(msg);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
client.send(msg).catch((err) => {
|
|
138
153
|
this.log.write("error", `mcp: http send: ${this.asError(err).message}`);
|
|
139
154
|
});
|
|
140
155
|
};
|
|
156
|
+
/* wire stdio server */
|
|
157
|
+
server.onmessage = sendToClient;
|
|
141
158
|
server.onerror = (err) => {
|
|
142
159
|
this.log.write("error", `mcp: stdio: ${err.message}`);
|
|
143
160
|
};
|
package/dst/ase-meta.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import fs from "node:fs";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
|
-
import { writeStdout } from "./ase-
|
|
9
|
+
import { writeStdout } from "./ase-stdio.js";
|
|
10
10
|
/* reusable functionality: resolve and read plugin "meta/" files */
|
|
11
11
|
export class Meta {
|
|
12
12
|
/* determine the plugin "meta/" directory; the build process copies
|
package/dst/ase-persona.js
CHANGED
|
@@ -20,9 +20,7 @@ export class Persona {
|
|
|
20
20
|
if (val === undefined)
|
|
21
21
|
return "engineer";
|
|
22
22
|
const style = String(isScalar(val) ? val.value : val);
|
|
23
|
-
|
|
24
|
-
return "engineer";
|
|
25
|
-
return style;
|
|
23
|
+
return Persona.styles.find((s) => s === style) ?? "engineer";
|
|
26
24
|
}
|
|
27
25
|
/* set the persona style on the strongest scope of an optional session */
|
|
28
26
|
static set(log, style, session) {
|
package/dst/ase-service.js
CHANGED
|
@@ -477,6 +477,16 @@ export default class ServiceCommand {
|
|
|
477
477
|
}
|
|
478
478
|
throw lastErr;
|
|
479
479
|
}
|
|
480
|
+
/* build the "POST /command" request options for a command token */
|
|
481
|
+
commandRequest(cmd, timeoutMs) {
|
|
482
|
+
return {
|
|
483
|
+
method: "POST",
|
|
484
|
+
headers: { "Content-Type": "application/json" },
|
|
485
|
+
body: { command: cmd },
|
|
486
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
487
|
+
ignoreResponseError: true
|
|
488
|
+
};
|
|
489
|
+
}
|
|
480
490
|
/* status flow: report whether the service is running */
|
|
481
491
|
async doStatus() {
|
|
482
492
|
const ctx = this.loadContext();
|
|
@@ -484,15 +494,9 @@ export default class ServiceCommand {
|
|
|
484
494
|
process.stdout.write("service: not running (no port configured)\n");
|
|
485
495
|
return 1;
|
|
486
496
|
}
|
|
487
|
-
const match = await probe(ctx.port, ctx.projectId);
|
|
497
|
+
const match = await probe(ctx.port, ctx.projectId).catch(() => null);
|
|
488
498
|
if (match === true) {
|
|
489
|
-
const r = await ofetch.raw(`http://${HOST}:${ctx.port}/command`,
|
|
490
|
-
method: "POST",
|
|
491
|
-
headers: { "Content-Type": "application/json" },
|
|
492
|
-
body: { command: "status" },
|
|
493
|
-
signal: AbortSignal.timeout(2000),
|
|
494
|
-
ignoreResponseError: true
|
|
495
|
-
});
|
|
499
|
+
const r = await ofetch.raw(`http://${HOST}:${ctx.port}/command`, this.commandRequest("status", 2000));
|
|
496
500
|
const d = r._data;
|
|
497
501
|
const uptimeMs = typeof d?.uptimeMs === "number" ? d.uptimeMs : 0;
|
|
498
502
|
const uptime = prettyMs(uptimeMs, { verbose: true });
|
|
@@ -509,30 +513,16 @@ export default class ServiceCommand {
|
|
|
509
513
|
/* send command: POST /command with the arbitrary cmd token */
|
|
510
514
|
async doSend(cmd) {
|
|
511
515
|
let ctx = this.loadContext();
|
|
512
|
-
if (ctx.port === null) {
|
|
513
|
-
|
|
514
|
-
ctx = this.loadContext();
|
|
515
|
-
if (ctx.port === null)
|
|
516
|
-
throw new Error("service not running (no port configured after auto-start)");
|
|
517
|
-
}
|
|
518
|
-
let match = await probe(ctx.port, ctx.projectId);
|
|
519
|
-
if (match !== true) {
|
|
516
|
+
if (ctx.port === null || await probe(ctx.port, ctx.projectId).catch(() => null) !== true) {
|
|
517
|
+
/* auto-start the service once, then re-check */
|
|
520
518
|
await this.doStart();
|
|
521
519
|
ctx = this.loadContext();
|
|
522
520
|
if (ctx.port === null)
|
|
523
521
|
throw new Error("service not running (no port configured after auto-start)");
|
|
524
|
-
|
|
525
|
-
if (match !== true)
|
|
522
|
+
if (await probe(ctx.port, ctx.projectId).catch(() => null) !== true)
|
|
526
523
|
throw new Error(`service not responding on port ${ctx.port} after auto-start`);
|
|
527
524
|
}
|
|
528
|
-
const r = await ofetch.raw(`http://${HOST}:${ctx.port}/command`, {
|
|
529
|
-
method: "POST",
|
|
530
|
-
headers: { "Content-Type": "application/json" },
|
|
531
|
-
body: { command: cmd },
|
|
532
|
-
signal: AbortSignal.timeout(5000),
|
|
533
|
-
ignoreResponseError: true,
|
|
534
|
-
responseType: "text"
|
|
535
|
-
});
|
|
525
|
+
const r = await ofetch.raw(`http://${HOST}:${ctx.port}/command`, { ...this.commandRequest(cmd, 5000), responseType: "text" });
|
|
536
526
|
const body = typeof r._data === "string" ? r._data : JSON.stringify(r._data);
|
|
537
527
|
process.stdout.write(body);
|
|
538
528
|
if (!body.endsWith("\n"))
|
|
@@ -546,7 +536,7 @@ export default class ServiceCommand {
|
|
|
546
536
|
this.log.write("info", "service: not running (no port configured)");
|
|
547
537
|
return 0;
|
|
548
538
|
}
|
|
549
|
-
const match = await probe(ctx.port, ctx.projectId);
|
|
539
|
+
const match = await probe(ctx.port, ctx.projectId).catch(() => null);
|
|
550
540
|
if (match === false) {
|
|
551
541
|
this.log.write("info", `service: not running (port ${ctx.port} in use by foreign service)`);
|
|
552
542
|
return 1;
|
package/dst/ase-setup.js
CHANGED
|
@@ -63,27 +63,18 @@ export default class SetupCommand {
|
|
|
63
63
|
path.join(prefix, "bin"),
|
|
64
64
|
path.join(prefix, "lib", "node_modules")
|
|
65
65
|
];
|
|
66
|
+
const accessible = (dir, mode) => fs.access(dir, mode).then(() => true, () => false);
|
|
66
67
|
for (const dir of candidates) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
catch {
|
|
78
|
-
/* directory does not exist: check parent writability */
|
|
79
|
-
try {
|
|
80
|
-
await fs.access(path.dirname(dir), fs.constants.W_OK);
|
|
81
|
-
}
|
|
82
|
-
catch {
|
|
83
|
-
return true;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
68
|
+
/* a writable directory needs no elevation at all */
|
|
69
|
+
if (await accessible(dir, fs.constants.W_OK))
|
|
70
|
+
continue;
|
|
71
|
+
/* the directory exists, but is not writable: require sudo */
|
|
72
|
+
if (await accessible(dir, fs.constants.F_OK))
|
|
73
|
+
return true;
|
|
74
|
+
/* the directory does not exist: require sudo unless it can
|
|
75
|
+
be created inside a writable parent directory */
|
|
76
|
+
if (!(await accessible(path.dirname(dir), fs.constants.W_OK)))
|
|
77
|
+
return true;
|
|
87
78
|
}
|
|
88
79
|
return false;
|
|
89
80
|
}
|
|
@@ -110,25 +101,7 @@ export default class SetupCommand {
|
|
|
110
101
|
for (let i = 0; i < retries; i++) {
|
|
111
102
|
const final = (i === retries - 1);
|
|
112
103
|
try {
|
|
113
|
-
|
|
114
|
-
const result = await execa(cmd, args, { stdio: "ignore", cwd, reject: false });
|
|
115
|
-
if (typeof result.exitCode === "number" && result.exitCode !== 0) {
|
|
116
|
-
if (!final) {
|
|
117
|
-
this.log.write("info", `setup: attempt ${i + 1}/${retries} failed for "${cmd} ${args.join(" ")}" ` +
|
|
118
|
-
`(exit code: ${result.exitCode}): retrying...`);
|
|
119
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
if (ignoreError !== undefined) {
|
|
123
|
-
this.log.write("info", `setup: ${ignoreError} (skipped)`);
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
this.log.write("error", `setup: command failed: exit code: ${result.exitCode}`);
|
|
127
|
-
throw new Error(`command "${cmd} ${args.join(" ")}" failed (exit code: ${result.exitCode})`);
|
|
128
|
-
}
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
await execa(cmd, args, { stdio: "pipe", cwd });
|
|
104
|
+
await execa(cmd, args, { stdio: quiet ? "ignore" : "pipe", cwd });
|
|
132
105
|
return;
|
|
133
106
|
}
|
|
134
107
|
catch (err) {
|
|
@@ -383,29 +356,13 @@ export default class SetupCommand {
|
|
|
383
356
|
args.push(name, transport.url);
|
|
384
357
|
}
|
|
385
358
|
}
|
|
386
|
-
else if (tool === "copilot") {
|
|
387
|
-
/* GitHub Copilot CLI implies the stdio transport when the
|
|
388
|
-
command is provided after "--"; only "http"/"sse" servers
|
|
389
|
-
need an explicit "--transport" flag and take the URL as a
|
|
390
|
-
positional argument */
|
|
391
|
-
if (transport.type === "stdio") {
|
|
392
|
-
args.push(name);
|
|
393
|
-
for (const [key, val] of Object.entries(env))
|
|
394
|
-
args.push("--env", `${key}=${val}`);
|
|
395
|
-
args.push("--", ...transport.command);
|
|
396
|
-
}
|
|
397
|
-
else {
|
|
398
|
-
args.push("--transport", "http");
|
|
399
|
-
for (const [key, val] of Object.entries(transport.headers ?? {}))
|
|
400
|
-
args.push("--header", `${key}: ${val}`);
|
|
401
|
-
args.push(name, transport.url);
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
359
|
else {
|
|
405
|
-
/*
|
|
406
|
-
positional argument and
|
|
407
|
-
command is provided after "--"; "http"
|
|
408
|
-
|
|
360
|
+
/* the GitHub Copilot CLI and OpenAI Codex CLI both take the
|
|
361
|
+
server name as a positional argument and imply the stdio
|
|
362
|
+
transport when the command is provided after "--"; for "http"
|
|
363
|
+
servers the GitHub Copilot CLI needs an explicit
|
|
364
|
+
"--transport" flag and takes the URL positionally, while the
|
|
365
|
+
OpenAI Codex CLI takes it via the "--url" option */
|
|
409
366
|
if (transport.type === "stdio") {
|
|
410
367
|
args.push(name);
|
|
411
368
|
for (const [key, val] of Object.entries(env))
|
|
@@ -413,9 +370,14 @@ export default class SetupCommand {
|
|
|
413
370
|
args.push("--", ...transport.command);
|
|
414
371
|
}
|
|
415
372
|
else {
|
|
373
|
+
if (tool === "copilot")
|
|
374
|
+
args.push("--transport", "http");
|
|
416
375
|
for (const [key, val] of Object.entries(transport.headers ?? {}))
|
|
417
376
|
args.push("--header", `${key}: ${val}`);
|
|
418
|
-
|
|
377
|
+
if (tool === "copilot")
|
|
378
|
+
args.push(name, transport.url);
|
|
379
|
+
else
|
|
380
|
+
args.push(name, "--url", transport.url);
|
|
419
381
|
}
|
|
420
382
|
}
|
|
421
383
|
await this.run(toolSpecs[tool].cli, args);
|
|
@@ -684,24 +646,18 @@ export default class SetupCommand {
|
|
|
684
646
|
AST, reproducing the exact whitespace tokens json-asty emits for a
|
|
685
647
|
canonically 4-space-indented settings.json */
|
|
686
648
|
statuslineBuildMember(root, command) {
|
|
687
|
-
const
|
|
688
|
-
const
|
|
689
|
-
const
|
|
690
|
-
const m = root.create("member");
|
|
691
|
-
if (epilog !== undefined)
|
|
692
|
-
m.set({ epilog });
|
|
693
|
-
return m.add(k, v);
|
|
694
|
-
};
|
|
695
|
-
const numMember = (key, val, epilog) => {
|
|
649
|
+
const member = (key, val, epilog) => {
|
|
650
|
+
const type = typeof val === "number" ? "number" : "string";
|
|
651
|
+
const body = typeof val === "number" ? String(val) : JSON.stringify(val);
|
|
696
652
|
const k = root.create("string").set({ body: JSON.stringify(key), value: key, epilog: ": " });
|
|
697
|
-
const v = root.create(
|
|
653
|
+
const v = root.create(type).set({ body, value: val });
|
|
698
654
|
const m = root.create("member");
|
|
699
655
|
if (epilog !== undefined)
|
|
700
656
|
m.set({ epilog });
|
|
701
657
|
return m.add(k, v);
|
|
702
658
|
};
|
|
703
659
|
const obj = root.create("object").set({ prolog: "{\n ", epilog: "\n }\n" });
|
|
704
|
-
obj.add(
|
|
660
|
+
obj.add(member("type", "command", ",\n "), member("command", command, ",\n "), member("padding", 0));
|
|
705
661
|
const key = root.create("string").set({
|
|
706
662
|
body: JSON.stringify("statusLine"),
|
|
707
663
|
value: "statusLine",
|
package/dst/ase-skills.js
CHANGED
|
@@ -54,7 +54,7 @@ export class Skills {
|
|
|
54
54
|
}
|
|
55
55
|
/* fetch GitHub stars given a repository URL (or empty string) */
|
|
56
56
|
static async fetchStars(repository) {
|
|
57
|
-
const m =
|
|
57
|
+
const m = /^(?:.*?:\/\/)?(?:[^@/]*@)?github\.com[:/]([^/]+\/[^/#?]+?)(?:\.git)?(?:[/#?].*)?$/.exec(repository);
|
|
58
58
|
if (m === null)
|
|
59
59
|
return "N.A.";
|
|
60
60
|
try {
|