shraga 0.1.50 → 0.1.51

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shraga",
3
- "version": "0.1.50",
3
+ "version": "0.1.51",
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",
@@ -1,9 +1,8 @@
1
1
  import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync } from 'node:fs';
2
2
  import { readFile, stat } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
- import { hostname } from 'node:os';
5
4
  import { DATA_DIR } from './paths.ts';
6
- import { emitEvent } from './events/bus.ts';
5
+ import { notifyOwners } from './notify-owners.ts';
7
6
  import { runTextQuery } from './sdk-utils.ts';
8
7
 
9
8
  const TAG = '[data-sync]';
@@ -576,24 +575,10 @@ export class DataSync {
576
575
  console.warn(`${TAG} notification suppressed (not the authoritative instance):\n${text}`);
577
576
  return;
578
577
  }
579
- // No Slack coupling here — resolve owner contacts and publish a deploy notice on the event bus.
580
- // The Slack feature (slackFeature) subscribes and DMs each owner. Owner resolution uses the
581
- // contacts store only, so data-sync stays transport-agnostic.
582
- const { getAll } = await import('./contacts.ts');
583
- const ownerEmails = (process.env.OWNERS ?? '').split(',').map(s => s.trim().toLowerCase()).filter(Boolean);
584
- const owners = getAll()
585
- .filter(c => c.slackIds.length > 0 && c.emails.some(e => ownerEmails.includes(e.toLowerCase())))
586
- .map(c => ({ name: c.name, slackId: c.slackIds[0] }));
587
- if (!owners.length) {
588
- console.warn(`${TAG} No owners (OWNERS env) with Slack IDs found, skipping notification`);
589
- return;
590
- }
591
- // Stamp the sender: every owner alert must name the instance it came from, so "is this back?"
592
- // is answerable without log archaeology across hosts.
593
- const from = `${this.options.deploymentId || 'shraga'}@${hostname()}`;
594
- emitEvent('data-sync', { kind: 'deploy', owners, text: `${text}\n\n_from ${from}_` });
578
+ await notifyOwners('data-sync', text);
595
579
  }
596
580
 
581
+
597
582
  /**
598
583
  * Routed through the Claude Code SDK (runTextQuery), same as the rest of the platform —
599
584
  * authenticates via the CC subscription, no ANTHROPIC_API_KEY required. Past incident:
@@ -0,0 +1,41 @@
1
+ // Reaching a human, transport-agnostically. Resolve the deployment's owners from the contacts
2
+ // store and publish a "deploy notice" on the event bus; the Slack feature subscribes and DMs each
3
+ // one. Callers stay free of Slack (and of owner-resolution) entirely.
4
+ //
5
+ // This lives on its own because more than one subsystem needs it — data-sync's merge/integrity
6
+ // alerts and self-upgrade's outcome report — and the second one silently had NO delivery at all:
7
+ // it emitted an event nobody subscribed to, so a finished upgrade never reached anyone.
8
+ import { hostname } from 'node:os';
9
+ import { emitEvent } from './events/bus.ts';
10
+
11
+ export type Owner = { name?: string; slackId: string };
12
+
13
+ /** Owners of THIS deployment (OWNERS env ∩ contacts that have a Slack id). */
14
+ export async function resolveOwners(): Promise<Owner[]> {
15
+ const { getAll } = await import('./contacts.ts');
16
+ const ownerEmails = (process.env.OWNERS ?? '').split(',').map(s => s.trim().toLowerCase()).filter(Boolean);
17
+ return getAll()
18
+ .filter(c => c.slackIds.length > 0 && c.emails.some(e => ownerEmails.includes(e.toLowerCase())))
19
+ .map(c => ({ name: c.name, slackId: c.slackIds[0] }));
20
+ }
21
+
22
+ /** `APP_NAME@host` — every owner-facing alert names the instance it came from, so "is this back?"
23
+ * is answerable without log archaeology across hosts. */
24
+ export function senderStamp(): string {
25
+ return `${process.env.APP_NAME || 'shraga'}@${hostname()}`;
26
+ }
27
+
28
+ /**
29
+ * DM the owners. `source` is the event-bus source (used for logging/filtering only) — delivery is
30
+ * keyed on the notice `kind`, so a new subsystem needs no change on the Slack side.
31
+ * Returns false when there was nobody to tell.
32
+ */
33
+ export async function notifyOwners(source: string, text: string): Promise<boolean> {
34
+ const owners = await resolveOwners();
35
+ if (!owners.length) {
36
+ console.warn(`[${source}] No owners (OWNERS env) with Slack IDs found, skipping notification`);
37
+ return false;
38
+ }
39
+ emitEvent(source as any, { kind: 'deploy', owners, text: `${text}\n\n_from ${senderStamp()}_` });
40
+ return true;
41
+ }
@@ -14,6 +14,7 @@ import { spawn, spawnSync } from 'node:child_process';
14
14
  import path from 'node:path';
15
15
  import { APP_ROOT, PACKAGE_ROOT, dataPath } from '../paths.ts';
16
16
  import { emitEvent } from '../events/bus.ts';
17
+ import { notifyOwners } from '../notify-owners.ts';
17
18
 
18
19
  const TAG = '[self-upgrade]';
19
20
  const PKG = 'shraga';
@@ -191,6 +192,12 @@ export class SelfUpgrade {
191
192
 
192
193
  console.log(`${TAG} ${report.status}: ${report.detail}`);
193
194
  emitEvent('self-upgrade.finished', report);
195
+ // ...and actually tell a human. The event alone reached nobody: no subscriber existed, so every
196
+ // upgrade outcome — including `revert-failed`, which needs hands — was silently dropped.
197
+ const icon = report.status === 'ok' ? '✅' : report.status === 'reverted' ? '↩️' : '🚨';
198
+ notifyOwners('self-upgrade', `${icon} Self-upgrade ${report.status}: ${report.detail}\n\n` +
199
+ `${report.package} ${report.from} → ${report.target} (now on ${report.installed})\nLog: \`${report.log}\``)
200
+ .catch(err => console.warn(`${TAG} could not notify owners:`, (err as Error).message));
194
201
  return report;
195
202
  }
196
203
 
@@ -25,11 +25,12 @@ export const slackFeature: ServerFeature = {
25
25
 
26
26
  if (!oauthMounted) { oauthMounted = true; registerSlackOAuthRoutes(ctx.app); }
27
27
 
28
- // Data-sync deploy notices arrive on the event bus (data-sync.ts has no Slack coupling); DM owners.
28
+ // Owner notices arrive on the event bus (see notify-owners.ts no subsystem couples to Slack);
29
+ // DM owners. Keyed on the notice KIND, not the source: self-upgrade emitted its outcome under
30
+ // its own source and a source-gated subscriber silently dropped every one of them.
29
31
  if (!ctx.passive && !busSubscribed) {
30
32
  busSubscribed = true;
31
33
  subscribeEvents((evt) => {
32
- if (evt.source !== 'data-sync') return;
33
34
  const payload = evt.payload as DeployNotice;
34
35
  if (payload?.kind !== 'deploy' || !payload.owners?.length) return;
35
36
  for (const owner of payload.owners) {