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`. Chain automations off it, e.g.:
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.24",
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",
@@ -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
- console.log(`[http] ${req.method} ${req.url} ${_res.statusCode} (${Date.now() - start}ms)`);
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)) { resolved[name] = server; continue; }
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
- status = abortController.signal.aborted ? 'aborted' : 'error';
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: `❌ Job failed.\n\n${prefix}${error}\n\`\`\`` }],
352
+ blocks: [{ type: 'text', text: `${killed ? '⏹️ Job interrupted' : '❌ Job failed'}.\n\n${prefix}${error}\n\`\`\`` }],
347
353
  });
348
- console.error(`[scheduler] job run error for ${schedule.id}:`, error);
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
- else reject(new Error(trimmed || `Command failed with exit code ${code}`));
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
  }