shraga 0.1.24 → 0.1.26
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.
|
@@ -156,7 +156,7 @@ For **vendor webhooks** that can't send shraga auth (Stripe, GitHub, …), add a
|
|
|
156
156
|
|
|
157
157
|
The system emits these onto the bus automatically — use them as the `source` of an event trigger to react to the agent's own lifecycle:
|
|
158
158
|
|
|
159
|
-
- **`schedule.finished`** — fired when any time/manual schedule run completes. Payload: `{ scheduleId, name, status, sessionId, sessionUrl?, error? }`. `status` is `ok` | `error` | `aborted
|
|
159
|
+
- **`schedule.finished`** — fired when any time/manual schedule run completes. Payload: `{ scheduleId, name, status, sessionId, sessionUrl?, error? }`. `status` is `ok` | `error` | `aborted` — `aborted` covers both a user cancel and a run killed by a signal (a deploy/restart SIGTERMs in-flight jobs), so alerts matching `error` don't fire on interrupted runs. Chain automations off it, e.g.:
|
|
160
160
|
```json
|
|
161
161
|
{ "trigger": { "kind": "event", "source": "schedule.finished", "match": { "status": "error" } },
|
|
162
162
|
"task": { "kind": "prompt", "prompt": "A scheduled run failed — investigate and post a summary." } }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shraga",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.26",
|
|
4
4
|
"description": "The teammate you delegate coding to — a self-hostable, multi-user AI coding agent web UI (Claude Code, with a pluggable engine seam).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
package/src/server/boot.ts
CHANGED
|
@@ -153,11 +153,18 @@ app.use(express.urlencoded({
|
|
|
153
153
|
verify: (req, _res, buf) => { (req as any).rawBody = buf; },
|
|
154
154
|
}));
|
|
155
155
|
|
|
156
|
+
// High-frequency poll routes (pty cwd, list refreshes) are quiet on success — they'd otherwise
|
|
157
|
+
// drown real request logs. Errors and everything else still logs unconditionally.
|
|
158
|
+
const QUIET_POLL_RE = /\/(cwd|ptys|sessions|workspace\/(pty-owners|layout))(\?|$)/;
|
|
159
|
+
|
|
156
160
|
app.use((req, _res, next) => {
|
|
157
161
|
const start = Date.now();
|
|
158
162
|
const orig = _res.end.bind(_res);
|
|
159
163
|
(_res as any).end = (...args: any[]) => {
|
|
160
|
-
|
|
164
|
+
const quiet = req.method === 'GET' && _res.statusCode < 400 && QUIET_POLL_RE.test(req.url);
|
|
165
|
+
if (!quiet) {
|
|
166
|
+
console.log(`[http] ${req.method} ${req.url} → ${_res.statusCode} (${Date.now() - start}ms)`);
|
|
167
|
+
}
|
|
161
168
|
return orig(...args);
|
|
162
169
|
};
|
|
163
170
|
next();
|
package/src/server/mcp.ts
CHANGED
|
@@ -52,11 +52,52 @@ function isHttpConfig(s: McpServerConfig): s is McpHttpServerConfig {
|
|
|
52
52
|
return s.type === 'http';
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/** `$VAR` / `${VAR}` placeholders inside a string. Same dialect as stdio `env` values, but also
|
|
56
|
+
* substitutes when EMBEDDED (`Bearer $TOKEN`) — a header value is never the bare variable. */
|
|
57
|
+
const PLACEHOLDER_RE = /\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))/g;
|
|
58
|
+
|
|
59
|
+
/** Expand placeholders; unset var → '' (same as stdio env resolution). `found` records each hit. */
|
|
60
|
+
function expandPlaceholders(value: string, found: string[]): string {
|
|
61
|
+
return value.replace(PLACEHOLDER_RE, (_m, braced, bare) => {
|
|
62
|
+
const v = process.env[braced || bare] || '';
|
|
63
|
+
found.push(v);
|
|
64
|
+
return v;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Resolve `$VAR` placeholders in an http entry's headers and url.
|
|
69
|
+
* Without this, an authenticated remote MCP can only be configured by INLINING the secret into
|
|
70
|
+
* data/mcps/*.json — which shraga auto-commits and pushes to the data repo (see saveMcpConfig →
|
|
71
|
+
* dataSync.trackWrite). Entries with no placeholders are returned untouched. */
|
|
72
|
+
function resolveHttpConfig(name: string, server: McpHttpServerConfig): McpHttpServerConfig | null {
|
|
73
|
+
const found: string[] = [];
|
|
74
|
+
const out: McpHttpServerConfig = { ...server };
|
|
75
|
+
if (typeof server.url === 'string') out.url = expandPlaceholders(server.url, found);
|
|
76
|
+
if (server.headers) {
|
|
77
|
+
const headers: Record<string, string> = {};
|
|
78
|
+
for (const [k, v] of Object.entries(server.headers)) {
|
|
79
|
+
headers[k] = typeof v === 'string' ? expandPlaceholders(v, found) : String(v ?? '');
|
|
80
|
+
}
|
|
81
|
+
out.headers = headers;
|
|
82
|
+
}
|
|
83
|
+
// Env-gate, mirroring stdio: an entry that DECLARES placeholders but resolves them all empty isn't
|
|
84
|
+
// configured for this deployment — skip it rather than mounting with a broken `Bearer ` credential.
|
|
85
|
+
if (found.length > 0 && found.every((v) => !v)) {
|
|
86
|
+
console.log(`[mcp] ${name}: skipped — required env placeholders not set in this deployment`);
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
|
|
55
92
|
/** Resolve env values: empty → process.env[same key], "$VAR" → process.env[VAR] */
|
|
56
93
|
function resolveEnv(config: McpConfig): McpConfig {
|
|
57
94
|
const resolved: McpConfig = {};
|
|
58
95
|
for (const [name, server] of Object.entries(config)) {
|
|
59
|
-
if (isHttpConfig(server)) {
|
|
96
|
+
if (isHttpConfig(server)) {
|
|
97
|
+
const http = resolveHttpConfig(name, server);
|
|
98
|
+
if (http) resolved[name] = http;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
60
101
|
if (!server.env) { resolved[name] = server; continue; }
|
|
61
102
|
const env: Record<string, string> = {};
|
|
62
103
|
for (const [k, v] of Object.entries(server.env)) {
|
|
@@ -338,14 +338,20 @@ async function runJobSchedule(
|
|
|
338
338
|
blocks: [{ type: 'text', text: `✅ Job completed successfully.\n\n${prefix}${output.trim() || '(no output)'}\n\`\`\`` }],
|
|
339
339
|
});
|
|
340
340
|
} catch (err: any) {
|
|
341
|
-
|
|
341
|
+
// A job killed by a SIGNAL was INTERRUPTED, not failed — the usual cause is systemd stopping the
|
|
342
|
+
// service (a deploy/restart), which SIGTERMs the whole cgroup including in-flight job children.
|
|
343
|
+
// Reporting that as `error` fires the failure notifier with an alert nobody can act on, whose
|
|
344
|
+
// "error" is just the job's own progress output (the process never got to throw anything).
|
|
345
|
+
// `aborted` is the existing status for "ended without failing" and the notifier ignores it.
|
|
346
|
+
const killed = typeof err?.signal === 'string';
|
|
347
|
+
status = abortController.signal.aborted || killed ? 'aborted' : 'error';
|
|
342
348
|
error = err?.message ?? String(err);
|
|
343
349
|
appendMessage(sessionId, {
|
|
344
350
|
id: crypto.randomUUID(),
|
|
345
351
|
role: 'assistant',
|
|
346
|
-
blocks: [{ type: 'text', text:
|
|
352
|
+
blocks: [{ type: 'text', text: `${killed ? '⏹️ Job interrupted' : '❌ Job failed'}.\n\n${prefix}${error}\n\`\`\`` }],
|
|
347
353
|
});
|
|
348
|
-
console
|
|
354
|
+
console[killed ? 'warn' : 'error'](`[scheduler] job run ${killed ? 'interrupted' : 'error'} for ${schedule.id}:`, error);
|
|
349
355
|
} finally {
|
|
350
356
|
clearInterval(partialInterval);
|
|
351
357
|
unregisterLivePartial(sessionId);
|
|
@@ -395,10 +401,19 @@ function runCommandWithMarker(command: string, abortController: AbortController,
|
|
|
395
401
|
|
|
396
402
|
abortController.signal.addEventListener('abort', () => child.kill('SIGTERM'), { once: true });
|
|
397
403
|
child.on('error', reject);
|
|
398
|
-
child.on('close', (code) => {
|
|
404
|
+
child.on('close', (code, signal) => {
|
|
399
405
|
const trimmed = output.trim();
|
|
400
|
-
if (code === 0) resolve(trimmed);
|
|
401
|
-
|
|
406
|
+
if (code === 0) return resolve(trimmed);
|
|
407
|
+
// Always state HOW it ended. The old form used the command's own output as the message whenever
|
|
408
|
+
// it had produced any, so a killed job reported its progress logs as "the error" and the actual
|
|
409
|
+
// cause (exit code / signal) was lost — the failure was captured, but not why.
|
|
410
|
+
const how = signal ? `killed by ${signal}` : `exit code ${code}`;
|
|
411
|
+
const err: NodeJS.ErrnoException & { signal?: string } = new Error(
|
|
412
|
+
trimmed ? `Command ${how}. Output:\n${trimmed}` : `Command ${how}`,
|
|
413
|
+
);
|
|
414
|
+
// Carried so callers can tell an interrupted run (deploy/restart) from a real failure.
|
|
415
|
+
if (signal) err.signal = signal;
|
|
416
|
+
reject(err);
|
|
402
417
|
});
|
|
403
418
|
});
|
|
404
419
|
}
|