@shepai/cli 1.26.0 → 1.27.0
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/dist/packages/core/src/application/ports/output/index.d.ts +1 -1
- package/dist/packages/core/src/application/ports/output/index.d.ts.map +1 -1
- package/dist/packages/core/src/application/ports/output/services/index.d.ts +1 -0
- package/dist/packages/core/src/application/ports/output/services/index.d.ts.map +1 -1
- package/dist/packages/core/src/application/ports/output/services/notification-service.interface.d.ts +29 -0
- package/dist/packages/core/src/application/ports/output/services/notification-service.interface.d.ts.map +1 -0
- package/dist/packages/core/src/application/ports/output/services/notification-service.interface.js +12 -0
- package/dist/packages/core/src/application/use-cases/features/create/metadata-generator.d.ts +3 -2
- package/dist/packages/core/src/application/use-cases/features/create/metadata-generator.d.ts.map +1 -1
- package/dist/packages/core/src/application/use-cases/features/create/metadata-generator.js +36 -15
- package/dist/packages/core/src/domain/factories/settings-defaults.factory.d.ts.map +1 -1
- package/dist/packages/core/src/domain/factories/settings-defaults.factory.js +13 -0
- package/dist/packages/core/src/domain/generated/output.d.ts +105 -0
- package/dist/packages/core/src/domain/generated/output.d.ts.map +1 -1
- package/dist/packages/core/src/domain/generated/output.js +15 -0
- package/dist/packages/core/src/infrastructure/di/container.d.ts.map +1 -1
- package/dist/packages/core/src/infrastructure/di/container.js +17 -0
- package/dist/packages/core/src/infrastructure/persistence/sqlite/mappers/settings.mapper.d.ts +8 -0
- package/dist/packages/core/src/infrastructure/persistence/sqlite/mappers/settings.mapper.d.ts.map +1 -1
- package/dist/packages/core/src/infrastructure/persistence/sqlite/mappers/settings.mapper.js +22 -0
- package/dist/packages/core/src/infrastructure/persistence/sqlite/migrations.d.ts.map +1 -1
- package/dist/packages/core/src/infrastructure/persistence/sqlite/migrations.js +14 -0
- package/dist/packages/core/src/infrastructure/repositories/sqlite-settings.repository.d.ts.map +1 -1
- package/dist/packages/core/src/infrastructure/repositories/sqlite-settings.repository.js +17 -3
- package/dist/packages/core/src/infrastructure/services/notifications/desktop-notifier.d.ts +22 -0
- package/dist/packages/core/src/infrastructure/services/notifications/desktop-notifier.d.ts.map +1 -0
- package/dist/packages/core/src/infrastructure/services/notifications/desktop-notifier.js +39 -0
- package/dist/packages/core/src/infrastructure/services/notifications/notification-bus.d.ts +52 -0
- package/dist/packages/core/src/infrastructure/services/notifications/notification-bus.d.ts.map +1 -0
- package/dist/packages/core/src/infrastructure/services/notifications/notification-bus.js +64 -0
- package/dist/packages/core/src/infrastructure/services/notifications/notification-watcher.service.d.ts +60 -0
- package/dist/packages/core/src/infrastructure/services/notifications/notification-watcher.service.d.ts.map +1 -0
- package/dist/packages/core/src/infrastructure/services/notifications/notification-watcher.service.js +225 -0
- package/dist/packages/core/src/infrastructure/services/notifications/notification.service.d.ts +21 -0
- package/dist/packages/core/src/infrastructure/services/notifications/notification.service.d.ts.map +1 -0
- package/dist/packages/core/src/infrastructure/services/notifications/notification.service.js +43 -0
- package/dist/src/presentation/cli/commands/ui.command.d.ts.map +1 -1
- package/dist/src/presentation/cli/commands/ui.command.js +8 -0
- package/dist/src/presentation/web/app/api/agent-events/route.d.ts +14 -0
- package/dist/src/presentation/web/app/api/agent-events/route.d.ts.map +1 -0
- package/dist/src/presentation/web/app/api/agent-events/route.js +77 -0
- package/dist/src/presentation/web/app/page.d.ts.map +1 -1
- package/dist/src/presentation/web/app/page.js +9 -12
- package/dist/src/presentation/web/components/common/feature-drawer/feature-drawer.js +2 -2
- package/dist/src/presentation/web/components/common/feature-drawer/feature-drawer.stories.js +3 -3
- package/dist/src/presentation/web/components/common/feature-node/feature-node-state-config.d.ts +5 -2
- package/dist/src/presentation/web/components/common/feature-node/feature-node-state-config.d.ts.map +1 -1
- package/dist/src/presentation/web/components/common/feature-node/feature-node-state-config.js +9 -0
- package/dist/src/presentation/web/components/common/feature-node/feature-node.d.ts.map +1 -1
- package/dist/src/presentation/web/components/common/feature-node/feature-node.js +9 -7
- package/dist/src/presentation/web/components/layouts/app-shell/app-shell.d.ts.map +1 -1
- package/dist/src/presentation/web/components/layouts/app-shell/app-shell.js +3 -0
- package/dist/src/presentation/web/components/ui/button.d.ts +1 -1
- package/dist/src/presentation/web/hooks/use-agent-events.d.ts +12 -0
- package/dist/src/presentation/web/hooks/use-agent-events.d.ts.map +1 -0
- package/dist/src/presentation/web/hooks/use-agent-events.js +58 -0
- package/dist/src/presentation/web/hooks/use-notifications.d.ts +10 -0
- package/dist/src/presentation/web/hooks/use-notifications.d.ts.map +1 -0
- package/dist/src/presentation/web/hooks/use-notifications.js +57 -0
- package/dist/src/presentation/web/hooks/use-notifications.stories.d.ts +6 -0
- package/dist/src/presentation/web/hooks/use-notifications.stories.d.ts.map +1 -0
- package/dist/src/presentation/web/hooks/use-notifications.stories.js +62 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +3 -1
- package/web/.next/BUILD_ID +1 -1
- package/web/.next/app-path-routes-manifest.json +1 -0
- package/web/.next/build-manifest.json +2 -2
- package/web/.next/cache/.previewinfo +1 -1
- package/web/.next/cache/.rscinfo +1 -1
- package/web/.next/cache/.tsbuildinfo +1 -1
- package/web/.next/cache/config.json +3 -3
- package/web/.next/fallback-build-manifest.json +2 -2
- package/web/.next/prerender-manifest.json +3 -3
- package/web/.next/required-server-files.js +1 -1
- package/web/.next/required-server-files.json +1 -1
- package/web/.next/routes-manifest.json +6 -0
- package/web/.next/server/app/_global-error.html +2 -2
- package/web/.next/server/app/_global-error.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/_not-found.html +2 -2
- package/web/.next/server/app/_not-found.rsc +10 -10
- package/web/.next/server/app/_not-found.segments/_full.segment.rsc +10 -10
- package/web/.next/server/app/_not-found.segments/_head.segment.rsc +3 -3
- package/web/.next/server/app/_not-found.segments/_index.segment.rsc +6 -6
- package/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- package/web/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/web/.next/server/app/api/agent-events/route/app-paths-manifest.json +3 -0
- package/web/.next/server/app/api/agent-events/route/build-manifest.json +11 -0
- package/web/.next/server/app/api/agent-events/route/server-reference-manifest.json +4 -0
- package/web/.next/server/app/api/agent-events/route.js +6 -0
- package/web/.next/server/app/api/agent-events/route.js.map +5 -0
- package/web/.next/server/app/api/agent-events/route.js.nft.json +1 -0
- package/web/.next/server/app/api/agent-events/route_client-reference-manifest.js +2 -0
- package/web/.next/server/app/api/dialog/pick-files/route.js +3 -2
- package/web/.next/server/app/api/dialog/pick-files/route.js.nft.json +1 -1
- package/web/.next/server/app/api/dialog/pick-folder/route.js +3 -2
- package/web/.next/server/app/api/dialog/pick-folder/route.js.nft.json +1 -1
- package/web/.next/server/app/api/features/create/route.js +3 -2
- package/web/.next/server/app/api/features/create/route.js.nft.json +1 -1
- package/web/.next/server/app/api/ide/open/route.js +4 -2
- package/web/.next/server/app/api/ide/open/route.js.nft.json +1 -1
- package/web/.next/server/app/api/shell/open/route.js +3 -2
- package/web/.next/server/app/api/shell/open/route.js.nft.json +1 -1
- package/web/.next/server/app/page.js +1 -1
- package/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/version/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/version.html +2 -2
- package/web/.next/server/app/version.rsc +5 -5
- package/web/.next/server/app/version.segments/_full.segment.rsc +5 -5
- package/web/.next/server/app/version.segments/_head.segment.rsc +1 -1
- package/web/.next/server/app/version.segments/_index.segment.rsc +4 -4
- package/web/.next/server/app/version.segments/_tree.segment.rsc +2 -2
- package/web/.next/server/app/version.segments/version/__PAGE__.segment.rsc +2 -2
- package/web/.next/server/app/version.segments/version.segment.rsc +1 -1
- package/web/.next/server/app-paths-manifest.json +1 -0
- package/web/.next/server/chunks/403f9_next_567de315._.js +17 -0
- package/web/.next/server/chunks/403f9_next_567de315._.js.map +1 -0
- package/web/.next/server/chunks/403f9_next_dist_esm_build_templates_app-route_12059e3e.js +3 -0
- package/web/.next/server/chunks/403f9_next_dist_esm_build_templates_app-route_12059e3e.js.map +1 -0
- package/web/.next/server/chunks/744ca_web__next-internal_server_app_api_agent-events_route_actions_686edb08.js +3 -0
- package/web/.next/server/chunks/744ca_web__next-internal_server_app_api_agent-events_route_actions_686edb08.js.map +1 -0
- package/web/.next/server/chunks/{[root-of-the-server]__eaa478b7._.js → [root-of-the-server]__4e987cf3._.js} +2 -2
- package/web/.next/server/chunks/[root-of-the-server]__57104a4c._.js +7 -0
- package/web/.next/server/chunks/[root-of-the-server]__57104a4c._.js.map +1 -0
- package/web/.next/server/chunks/{[root-of-the-server]__602cce1b._.js → [root-of-the-server]__61349adc._.js} +2 -2
- package/web/.next/server/chunks/{[root-of-the-server]__bb4fc5b5._.js → [root-of-the-server]__a6bf88cc._.js} +2 -2
- package/web/.next/server/chunks/[root-of-the-server]__aa2074ca._.js +3 -0
- package/web/.next/server/chunks/[root-of-the-server]__aa2074ca._.js.map +1 -0
- package/web/.next/server/chunks/{[root-of-the-server]__1913013c._.js → [root-of-the-server]__ec87d735._.js} +2 -2
- package/web/.next/server/chunks/[root-of-the-server]__ecaf05bc._.js +3 -0
- package/web/.next/server/chunks/[root-of-the-server]__ecaf05bc._.js.map +1 -0
- package/web/.next/server/chunks/ssr/403f9_next_dist_esm_build_templates_app-page_dbf21e04.js +1 -1
- package/web/.next/server/chunks/ssr/[root-of-the-server]__80e5bed2._.js +1 -1
- package/web/.next/server/chunks/ssr/[root-of-the-server]__f12b60be._.js +1 -1
- package/web/.next/server/chunks/ssr/[root-of-the-server]__fa87595c._.js +1 -1
- package/web/.next/server/chunks/ssr/_09afa42a._.js +1 -1
- package/web/.next/server/chunks/ssr/_09afa42a._.js.map +1 -1
- package/web/.next/server/chunks/ssr/_884ddade._.js +1 -1
- package/web/.next/server/chunks/ssr/_c5d377cc._.js +1 -1
- package/web/.next/server/chunks/ssr/src_presentation_web_components_e599bb8c._.js +1 -1
- package/web/.next/server/chunks/ssr/src_presentation_web_components_e599bb8c._.js.map +1 -1
- package/web/.next/server/chunks/ssr/src_presentation_web_ed0934e5._.js +1 -1
- package/web/.next/server/chunks/ssr/src_presentation_web_ed0934e5._.js.map +1 -1
- package/web/.next/server/pages/404.html +2 -2
- package/web/.next/server/pages/500.html +2 -2
- package/web/.next/server/server-reference-manifest.js +1 -1
- package/web/.next/server/server-reference-manifest.json +1 -1
- package/web/.next/static/chunks/b32b17f102226f32.js +1 -0
- package/web/.next/static/chunks/d8905884f147599c.css +2 -0
- package/web/.next/static/chunks/{c2854b6c6d0ca1f3.js → fd961ef835ff4867.js} +2 -2
- package/web/.next/trace +1 -1
- package/web/.next/trace-build +1 -1
- package/web/.next/types/link.d.ts +1 -0
- package/web/.next/types/routes.d.ts +2 -1
- package/web/.next/types/validator.ts +9 -0
- package/web/.next/server/chunks/[root-of-the-server]__56ac1509._.js +0 -3
- package/web/.next/server/chunks/[root-of-the-server]__56ac1509._.js.map +0 -1
- package/web/.next/server/chunks/[root-of-the-server]__ba6c5641._.js +0 -21
- package/web/.next/server/chunks/[root-of-the-server]__ba6c5641._.js.map +0 -1
- package/web/.next/static/chunks/78ef4acb26e4d6ea.js +0 -1
- package/web/.next/static/chunks/86ff2b6444e06e88.css +0 -2
- /package/web/.next/server/chunks/{[root-of-the-server]__eaa478b7._.js.map → [root-of-the-server]__4e987cf3._.js.map} +0 -0
- /package/web/.next/server/chunks/{[root-of-the-server]__602cce1b._.js.map → [root-of-the-server]__61349adc._.js.map} +0 -0
- /package/web/.next/server/chunks/{[root-of-the-server]__bb4fc5b5._.js.map → [root-of-the-server]__a6bf88cc._.js.map} +0 -0
- /package/web/.next/server/chunks/{[root-of-the-server]__1913013c._.js.map → [root-of-the-server]__ec87d735._.js.map} +0 -0
- /package/web/.next/static/{_6j6GS-S6zPiASj5tsxJ8 → AmQVHj-yhxaqpAg8gC7MH}/_buildManifest.js +0 -0
- /package/web/.next/static/{_6j6GS-S6zPiASj5tsxJ8 → AmQVHj-yhxaqpAg8gC7MH}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/static/{_6j6GS-S6zPiASj5tsxJ8 → AmQVHj-yhxaqpAg8gC7MH}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Desktop Notifier
|
|
3
|
+
*
|
|
4
|
+
* Wraps node-notifier to send native OS desktop notifications
|
|
5
|
+
* (macOS Notification Center, Linux libnotify, Windows toast).
|
|
6
|
+
*
|
|
7
|
+
* All inputs are sanitized before passing to node-notifier:
|
|
8
|
+
* - Shell metacharacters are stripped (defense in depth)
|
|
9
|
+
* - Title truncated to 100 chars, body to 500 chars
|
|
10
|
+
* - Errors are caught and logged (never thrown)
|
|
11
|
+
*/
|
|
12
|
+
import notifier from 'node-notifier';
|
|
13
|
+
const SHELL_METACHAR_REGEX = /[`$|;&()<>]/g;
|
|
14
|
+
const MAX_TITLE_LENGTH = 100;
|
|
15
|
+
const MAX_BODY_LENGTH = 500;
|
|
16
|
+
export class DesktopNotifier {
|
|
17
|
+
/**
|
|
18
|
+
* Send a native OS desktop notification.
|
|
19
|
+
*
|
|
20
|
+
* @param title - Notification title (sanitized and truncated)
|
|
21
|
+
* @param body - Notification body text (sanitized and truncated)
|
|
22
|
+
*/
|
|
23
|
+
send(title, body) {
|
|
24
|
+
try {
|
|
25
|
+
notifier.notify({
|
|
26
|
+
title: this.sanitize(title, MAX_TITLE_LENGTH),
|
|
27
|
+
message: this.sanitize(body, MAX_BODY_LENGTH),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
// eslint-disable-next-line no-console
|
|
32
|
+
console.warn('Desktop notification failed:', error);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
sanitize(input, maxLength) {
|
|
36
|
+
const cleaned = input.replace(SHELL_METACHAR_REGEX, '');
|
|
37
|
+
return cleaned.slice(0, maxLength);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notification Event Bus
|
|
3
|
+
*
|
|
4
|
+
* Typed EventEmitter singleton for in-process pub/sub of notification events.
|
|
5
|
+
* Follows the getSettings() singleton pattern from settings.service.ts.
|
|
6
|
+
*
|
|
7
|
+
* Multiple listeners can subscribe to the 'notification' event for fan-out
|
|
8
|
+
* delivery (SSE clients, desktop notifier, etc.).
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* ```typescript
|
|
12
|
+
* import { getNotificationBus } from './notification-bus.js';
|
|
13
|
+
*
|
|
14
|
+
* const bus = getNotificationBus();
|
|
15
|
+
* bus.on('notification', (event) => { ... });
|
|
16
|
+
* bus.emit('notification', event);
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
import { EventEmitter } from 'node:events';
|
|
20
|
+
import type { NotificationEvent } from '../../../domain/generated/output.js';
|
|
21
|
+
export interface NotificationEventMap {
|
|
22
|
+
notification: [event: NotificationEvent];
|
|
23
|
+
}
|
|
24
|
+
export type NotificationBus = EventEmitter<NotificationEventMap>;
|
|
25
|
+
/**
|
|
26
|
+
* Initialize the notification event bus singleton.
|
|
27
|
+
* Must be called once during CLI bootstrap.
|
|
28
|
+
*
|
|
29
|
+
* @throws Error if the bus is already initialized
|
|
30
|
+
*/
|
|
31
|
+
export declare function initializeNotificationBus(): void;
|
|
32
|
+
/**
|
|
33
|
+
* Get the notification event bus singleton.
|
|
34
|
+
*
|
|
35
|
+
* @returns The notification event bus
|
|
36
|
+
* @throws Error if the bus hasn't been initialized yet
|
|
37
|
+
*/
|
|
38
|
+
export declare function getNotificationBus(): NotificationBus;
|
|
39
|
+
/**
|
|
40
|
+
* Check if the notification bus has been initialized.
|
|
41
|
+
*
|
|
42
|
+
* @returns True if the bus is initialized, false otherwise
|
|
43
|
+
*/
|
|
44
|
+
export declare function hasNotificationBus(): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Reset the notification bus singleton (for testing purposes only).
|
|
47
|
+
* DO NOT use in production code.
|
|
48
|
+
*
|
|
49
|
+
* @internal
|
|
50
|
+
*/
|
|
51
|
+
export declare function resetNotificationBus(): void;
|
|
52
|
+
//# sourceMappingURL=notification-bus.d.ts.map
|
package/dist/packages/core/src/infrastructure/services/notifications/notification-bus.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notification-bus.d.ts","sourceRoot":"","sources":["../../../../../../../packages/core/src/infrastructure/services/notifications/notification-bus.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qCAAqC,CAAC;AAE7E,MAAM,WAAW,oBAAoB;IACnC,YAAY,EAAE,CAAC,KAAK,EAAE,iBAAiB,CAAC,CAAC;CAC1C;AAED,MAAM,MAAM,eAAe,GAAG,YAAY,CAAC,oBAAoB,CAAC,CAAC;AAIjE;;;;;GAKG;AACH,wBAAgB,yBAAyB,IAAI,IAAI,CAMhD;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,IAAI,eAAe,CAQpD;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,IAAI,OAAO,CAE5C;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAK3C"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notification Event Bus
|
|
3
|
+
*
|
|
4
|
+
* Typed EventEmitter singleton for in-process pub/sub of notification events.
|
|
5
|
+
* Follows the getSettings() singleton pattern from settings.service.ts.
|
|
6
|
+
*
|
|
7
|
+
* Multiple listeners can subscribe to the 'notification' event for fan-out
|
|
8
|
+
* delivery (SSE clients, desktop notifier, etc.).
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* ```typescript
|
|
12
|
+
* import { getNotificationBus } from './notification-bus.js';
|
|
13
|
+
*
|
|
14
|
+
* const bus = getNotificationBus();
|
|
15
|
+
* bus.on('notification', (event) => { ... });
|
|
16
|
+
* bus.emit('notification', event);
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
import { EventEmitter } from 'node:events';
|
|
20
|
+
let busInstance = null;
|
|
21
|
+
/**
|
|
22
|
+
* Initialize the notification event bus singleton.
|
|
23
|
+
* Must be called once during CLI bootstrap.
|
|
24
|
+
*
|
|
25
|
+
* @throws Error if the bus is already initialized
|
|
26
|
+
*/
|
|
27
|
+
export function initializeNotificationBus() {
|
|
28
|
+
if (busInstance !== null) {
|
|
29
|
+
throw new Error('Notification bus already initialized. Cannot re-initialize.');
|
|
30
|
+
}
|
|
31
|
+
busInstance = new EventEmitter();
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Get the notification event bus singleton.
|
|
35
|
+
*
|
|
36
|
+
* @returns The notification event bus
|
|
37
|
+
* @throws Error if the bus hasn't been initialized yet
|
|
38
|
+
*/
|
|
39
|
+
export function getNotificationBus() {
|
|
40
|
+
if (busInstance === null) {
|
|
41
|
+
throw new Error('Notification bus not initialized. Call initializeNotificationBus() during CLI bootstrap.');
|
|
42
|
+
}
|
|
43
|
+
return busInstance;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Check if the notification bus has been initialized.
|
|
47
|
+
*
|
|
48
|
+
* @returns True if the bus is initialized, false otherwise
|
|
49
|
+
*/
|
|
50
|
+
export function hasNotificationBus() {
|
|
51
|
+
return busInstance !== null;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Reset the notification bus singleton (for testing purposes only).
|
|
55
|
+
* DO NOT use in production code.
|
|
56
|
+
*
|
|
57
|
+
* @internal
|
|
58
|
+
*/
|
|
59
|
+
export function resetNotificationBus() {
|
|
60
|
+
if (busInstance !== null) {
|
|
61
|
+
busInstance.removeAllListeners();
|
|
62
|
+
}
|
|
63
|
+
busInstance = null;
|
|
64
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notification Watcher Service
|
|
3
|
+
*
|
|
4
|
+
* Polls the agent_runs and phase_timings tables to detect status
|
|
5
|
+
* transitions, dispatching NotificationEvents via the NotificationService
|
|
6
|
+
* (which handles settings filters, bus fan-out, and desktop dispatch).
|
|
7
|
+
*
|
|
8
|
+
* Maintains in-memory tracking of last-seen status per run to avoid
|
|
9
|
+
* duplicate notifications. Polling starts/stops with start()/stop()
|
|
10
|
+
* lifecycle methods.
|
|
11
|
+
*
|
|
12
|
+
* Design decision: Uses DB polling instead of IPC because the worker
|
|
13
|
+
* process is spawned detached with IPC disconnected.
|
|
14
|
+
*/
|
|
15
|
+
import type { IAgentRunRepository } from '../../../application/ports/output/agents/agent-run-repository.interface.js';
|
|
16
|
+
import type { IPhaseTimingRepository } from '../../../application/ports/output/agents/phase-timing-repository.interface.js';
|
|
17
|
+
import type { INotificationService } from '../../../application/ports/output/services/notification-service.interface.js';
|
|
18
|
+
export declare class NotificationWatcherService {
|
|
19
|
+
private readonly runRepository;
|
|
20
|
+
private readonly phaseTimingRepository;
|
|
21
|
+
private readonly notificationService;
|
|
22
|
+
private readonly pollIntervalMs;
|
|
23
|
+
private readonly trackedRuns;
|
|
24
|
+
private intervalId;
|
|
25
|
+
constructor(runRepository: IAgentRunRepository, phaseTimingRepository: IPhaseTimingRepository, notificationService: INotificationService, pollIntervalMs?: number);
|
|
26
|
+
isRunning(): boolean;
|
|
27
|
+
start(): void;
|
|
28
|
+
stop(): void;
|
|
29
|
+
private poll;
|
|
30
|
+
private processRuns;
|
|
31
|
+
private emitStatusEvent;
|
|
32
|
+
private checkPhaseCompletions;
|
|
33
|
+
private resolveFeatureName;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Initialize the notification watcher singleton.
|
|
37
|
+
* Must be called once during web server startup.
|
|
38
|
+
*
|
|
39
|
+
* @throws Error if the watcher is already initialized
|
|
40
|
+
*/
|
|
41
|
+
export declare function initializeNotificationWatcher(runRepository: IAgentRunRepository, phaseTimingRepository: IPhaseTimingRepository, notificationService: INotificationService, pollIntervalMs?: number): void;
|
|
42
|
+
/**
|
|
43
|
+
* Get the notification watcher singleton.
|
|
44
|
+
*
|
|
45
|
+
* @returns The notification watcher service
|
|
46
|
+
* @throws Error if the watcher hasn't been initialized yet
|
|
47
|
+
*/
|
|
48
|
+
export declare function getNotificationWatcher(): NotificationWatcherService;
|
|
49
|
+
/**
|
|
50
|
+
* Check if the notification watcher has been initialized.
|
|
51
|
+
*/
|
|
52
|
+
export declare function hasNotificationWatcher(): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Reset the notification watcher singleton (for testing purposes only).
|
|
55
|
+
* Stops the watcher if running before resetting.
|
|
56
|
+
*
|
|
57
|
+
* @internal
|
|
58
|
+
*/
|
|
59
|
+
export declare function resetNotificationWatcher(): void;
|
|
60
|
+
//# sourceMappingURL=notification-watcher.service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notification-watcher.service.d.ts","sourceRoot":"","sources":["../../../../../../../packages/core/src/infrastructure/services/notifications/notification-watcher.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAQH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4EAA4E,CAAC;AACtH,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,+EAA+E,CAAC;AAC5H,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,8EAA8E,CAAC;AAmDzH,qBAAa,0BAA0B;IACrC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAsB;IACpD,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAyB;IAC/D,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAuB;IAC3D,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAmC;IAC/D,OAAO,CAAC,UAAU,CAA+C;gBAG/D,aAAa,EAAE,mBAAmB,EAClC,qBAAqB,EAAE,sBAAsB,EAC7C,mBAAmB,EAAE,oBAAoB,EACzC,cAAc,GAAE,MAAiC;IAQnD,SAAS,IAAI,OAAO;IAIpB,KAAK,IAAI,IAAI;IAWb,IAAI,IAAI,IAAI;YAOE,IAAI;YAUJ,WAAW;IAoDzB,OAAO,CAAC,eAAe;YAgBT,qBAAqB;IAsBnC,OAAO,CAAC,kBAAkB;CAS3B;AAMD;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAC3C,aAAa,EAAE,mBAAmB,EAClC,qBAAqB,EAAE,sBAAsB,EAC7C,mBAAmB,EAAE,oBAAoB,EACzC,cAAc,CAAC,EAAE,MAAM,GACtB,IAAI,CAWN;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,IAAI,0BAA0B,CAQnE;AAED;;GAEG;AACH,wBAAgB,sBAAsB,IAAI,OAAO,CAEhD;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,IAAI,IAAI,CAK/C"}
|
package/dist/packages/core/src/infrastructure/services/notifications/notification-watcher.service.js
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notification Watcher Service
|
|
3
|
+
*
|
|
4
|
+
* Polls the agent_runs and phase_timings tables to detect status
|
|
5
|
+
* transitions, dispatching NotificationEvents via the NotificationService
|
|
6
|
+
* (which handles settings filters, bus fan-out, and desktop dispatch).
|
|
7
|
+
*
|
|
8
|
+
* Maintains in-memory tracking of last-seen status per run to avoid
|
|
9
|
+
* duplicate notifications. Polling starts/stops with start()/stop()
|
|
10
|
+
* lifecycle methods.
|
|
11
|
+
*
|
|
12
|
+
* Design decision: Uses DB polling instead of IPC because the worker
|
|
13
|
+
* process is spawned detached with IPC disconnected.
|
|
14
|
+
*/
|
|
15
|
+
import { AgentRunStatus, NotificationEventType, NotificationSeverity, } from '../../../domain/generated/output.js';
|
|
16
|
+
const DEFAULT_POLL_INTERVAL_MS = 3000;
|
|
17
|
+
const ACTIVE_STATUSES = new Set([
|
|
18
|
+
AgentRunStatus.pending,
|
|
19
|
+
AgentRunStatus.running,
|
|
20
|
+
AgentRunStatus.waitingApproval,
|
|
21
|
+
]);
|
|
22
|
+
const TERMINAL_STATUSES = new Set([
|
|
23
|
+
AgentRunStatus.completed,
|
|
24
|
+
AgentRunStatus.failed,
|
|
25
|
+
AgentRunStatus.cancelled,
|
|
26
|
+
AgentRunStatus.interrupted,
|
|
27
|
+
]);
|
|
28
|
+
const STATUS_TO_EVENT = {
|
|
29
|
+
[AgentRunStatus.running]: {
|
|
30
|
+
eventType: NotificationEventType.AgentStarted,
|
|
31
|
+
severity: NotificationSeverity.Info,
|
|
32
|
+
},
|
|
33
|
+
[AgentRunStatus.waitingApproval]: {
|
|
34
|
+
eventType: NotificationEventType.WaitingApproval,
|
|
35
|
+
severity: NotificationSeverity.Warning,
|
|
36
|
+
},
|
|
37
|
+
[AgentRunStatus.completed]: {
|
|
38
|
+
eventType: NotificationEventType.AgentCompleted,
|
|
39
|
+
severity: NotificationSeverity.Success,
|
|
40
|
+
},
|
|
41
|
+
[AgentRunStatus.failed]: {
|
|
42
|
+
eventType: NotificationEventType.AgentFailed,
|
|
43
|
+
severity: NotificationSeverity.Error,
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
const EVENT_MESSAGES = {
|
|
47
|
+
[NotificationEventType.AgentStarted]: 'Feature agent started',
|
|
48
|
+
[NotificationEventType.WaitingApproval]: 'Feature agent is waiting for approval',
|
|
49
|
+
[NotificationEventType.AgentCompleted]: 'Feature agent completed successfully',
|
|
50
|
+
[NotificationEventType.AgentFailed]: 'Feature agent failed',
|
|
51
|
+
};
|
|
52
|
+
export class NotificationWatcherService {
|
|
53
|
+
runRepository;
|
|
54
|
+
phaseTimingRepository;
|
|
55
|
+
notificationService;
|
|
56
|
+
pollIntervalMs;
|
|
57
|
+
trackedRuns = new Map();
|
|
58
|
+
intervalId = null;
|
|
59
|
+
constructor(runRepository, phaseTimingRepository, notificationService, pollIntervalMs = DEFAULT_POLL_INTERVAL_MS) {
|
|
60
|
+
this.runRepository = runRepository;
|
|
61
|
+
this.phaseTimingRepository = phaseTimingRepository;
|
|
62
|
+
this.notificationService = notificationService;
|
|
63
|
+
this.pollIntervalMs = pollIntervalMs;
|
|
64
|
+
}
|
|
65
|
+
isRunning() {
|
|
66
|
+
return this.intervalId !== null;
|
|
67
|
+
}
|
|
68
|
+
start() {
|
|
69
|
+
if (this.intervalId !== null)
|
|
70
|
+
return;
|
|
71
|
+
// Run first poll immediately
|
|
72
|
+
void this.poll();
|
|
73
|
+
this.intervalId = setInterval(() => {
|
|
74
|
+
void this.poll();
|
|
75
|
+
}, this.pollIntervalMs);
|
|
76
|
+
}
|
|
77
|
+
stop() {
|
|
78
|
+
if (this.intervalId !== null) {
|
|
79
|
+
clearInterval(this.intervalId);
|
|
80
|
+
this.intervalId = null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async poll() {
|
|
84
|
+
try {
|
|
85
|
+
const runs = await this.runRepository.list();
|
|
86
|
+
await this.processRuns(runs);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
// eslint-disable-next-line no-console
|
|
90
|
+
console.warn('NotificationWatcherService poll failed:', error);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async processRuns(runs) {
|
|
94
|
+
const currentRunIds = new Set();
|
|
95
|
+
for (const run of runs) {
|
|
96
|
+
if (!ACTIVE_STATUSES.has(run.status) && !this.trackedRuns.has(run.id)) {
|
|
97
|
+
// Skip terminal runs we haven't been tracking
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
currentRunIds.add(run.id);
|
|
101
|
+
const prevState = this.trackedRuns.get(run.id);
|
|
102
|
+
if (!prevState) {
|
|
103
|
+
// New run — track and emit initial status event
|
|
104
|
+
const featureName = this.resolveFeatureName(run);
|
|
105
|
+
const newState = {
|
|
106
|
+
status: run.status,
|
|
107
|
+
completedPhases: new Set(),
|
|
108
|
+
featureName,
|
|
109
|
+
};
|
|
110
|
+
this.trackedRuns.set(run.id, newState);
|
|
111
|
+
this.emitStatusEvent(run, newState);
|
|
112
|
+
// Check for completed phases on first observation
|
|
113
|
+
await this.checkPhaseCompletions(run.id, newState);
|
|
114
|
+
}
|
|
115
|
+
else if (prevState.status !== run.status) {
|
|
116
|
+
// Status changed — emit event
|
|
117
|
+
prevState.status = run.status;
|
|
118
|
+
this.emitStatusEvent(run, prevState);
|
|
119
|
+
if (TERMINAL_STATUSES.has(run.status)) {
|
|
120
|
+
// Run reached terminal state — clean up after emitting
|
|
121
|
+
this.trackedRuns.delete(run.id);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
// Still active — check for phase completions
|
|
125
|
+
await this.checkPhaseCompletions(run.id, prevState);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
// Same status — just check for phase completions
|
|
130
|
+
await this.checkPhaseCompletions(run.id, prevState);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// Clean up tracking for runs that disappeared from the list
|
|
134
|
+
for (const trackedId of this.trackedRuns.keys()) {
|
|
135
|
+
if (!currentRunIds.has(trackedId)) {
|
|
136
|
+
this.trackedRuns.delete(trackedId);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
emitStatusEvent(run, state) {
|
|
141
|
+
const mapping = STATUS_TO_EVENT[run.status];
|
|
142
|
+
if (!mapping)
|
|
143
|
+
return;
|
|
144
|
+
const event = {
|
|
145
|
+
eventType: mapping.eventType,
|
|
146
|
+
agentRunId: run.id,
|
|
147
|
+
featureName: state.featureName,
|
|
148
|
+
message: EVENT_MESSAGES[mapping.eventType] ?? `Agent status: ${run.status}`,
|
|
149
|
+
severity: mapping.severity,
|
|
150
|
+
timestamp: new Date().toISOString(),
|
|
151
|
+
};
|
|
152
|
+
this.notificationService.notify(event);
|
|
153
|
+
}
|
|
154
|
+
async checkPhaseCompletions(runId, state) {
|
|
155
|
+
const timings = await this.phaseTimingRepository.findByRunId(runId);
|
|
156
|
+
for (const timing of timings) {
|
|
157
|
+
if (timing.completedAt && !state.completedPhases.has(timing.phase)) {
|
|
158
|
+
state.completedPhases.add(timing.phase);
|
|
159
|
+
const event = {
|
|
160
|
+
eventType: NotificationEventType.PhaseCompleted,
|
|
161
|
+
agentRunId: runId,
|
|
162
|
+
featureName: state.featureName,
|
|
163
|
+
phaseName: timing.phase,
|
|
164
|
+
message: `Completed ${timing.phase} phase`,
|
|
165
|
+
severity: NotificationSeverity.Info,
|
|
166
|
+
timestamp: new Date().toISOString(),
|
|
167
|
+
};
|
|
168
|
+
this.notificationService.notify(event);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
resolveFeatureName(run) {
|
|
173
|
+
// Feature name resolution from featureId would require IFeatureRepository.
|
|
174
|
+
// For now, use the agentName or a fallback. The NotificationService or
|
|
175
|
+
// a higher layer can enrich with the actual feature name if needed.
|
|
176
|
+
if (run.featureId) {
|
|
177
|
+
return `Feature ${run.featureId}`;
|
|
178
|
+
}
|
|
179
|
+
return `Agent ${run.id}`;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// --- Singleton accessors (follows getNotificationBus() pattern) ---
|
|
183
|
+
let watcherInstance = null;
|
|
184
|
+
/**
|
|
185
|
+
* Initialize the notification watcher singleton.
|
|
186
|
+
* Must be called once during web server startup.
|
|
187
|
+
*
|
|
188
|
+
* @throws Error if the watcher is already initialized
|
|
189
|
+
*/
|
|
190
|
+
export function initializeNotificationWatcher(runRepository, phaseTimingRepository, notificationService, pollIntervalMs) {
|
|
191
|
+
if (watcherInstance !== null) {
|
|
192
|
+
throw new Error('Notification watcher already initialized. Cannot re-initialize.');
|
|
193
|
+
}
|
|
194
|
+
watcherInstance = new NotificationWatcherService(runRepository, phaseTimingRepository, notificationService, pollIntervalMs);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Get the notification watcher singleton.
|
|
198
|
+
*
|
|
199
|
+
* @returns The notification watcher service
|
|
200
|
+
* @throws Error if the watcher hasn't been initialized yet
|
|
201
|
+
*/
|
|
202
|
+
export function getNotificationWatcher() {
|
|
203
|
+
if (watcherInstance === null) {
|
|
204
|
+
throw new Error('Notification watcher not initialized. Call initializeNotificationWatcher() during web server startup.');
|
|
205
|
+
}
|
|
206
|
+
return watcherInstance;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Check if the notification watcher has been initialized.
|
|
210
|
+
*/
|
|
211
|
+
export function hasNotificationWatcher() {
|
|
212
|
+
return watcherInstance !== null;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Reset the notification watcher singleton (for testing purposes only).
|
|
216
|
+
* Stops the watcher if running before resetting.
|
|
217
|
+
*
|
|
218
|
+
* @internal
|
|
219
|
+
*/
|
|
220
|
+
export function resetNotificationWatcher() {
|
|
221
|
+
if (watcherInstance !== null) {
|
|
222
|
+
watcherInstance.stop();
|
|
223
|
+
}
|
|
224
|
+
watcherInstance = null;
|
|
225
|
+
}
|
package/dist/packages/core/src/infrastructure/services/notifications/notification.service.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notification Service
|
|
3
|
+
*
|
|
4
|
+
* Implements INotificationService port interface. Fans out notification
|
|
5
|
+
* events to enabled channels:
|
|
6
|
+
* - Notification bus (for SSE → in-app toasts and browser notifications)
|
|
7
|
+
* - Desktop notifier (node-notifier for OS-level notifications)
|
|
8
|
+
*
|
|
9
|
+
* Channel enable/disable and event type filters are read from Settings.
|
|
10
|
+
*/
|
|
11
|
+
import type { NotificationEvent } from '../../../domain/generated/output.js';
|
|
12
|
+
import type { INotificationService } from '../../../application/ports/output/services/notification-service.interface.js';
|
|
13
|
+
import type { NotificationBus } from './notification-bus.js';
|
|
14
|
+
import type { DesktopNotifier } from './desktop-notifier.js';
|
|
15
|
+
export declare class NotificationService implements INotificationService {
|
|
16
|
+
private readonly bus;
|
|
17
|
+
private readonly desktopNotifier;
|
|
18
|
+
constructor(bus: NotificationBus, desktopNotifier: DesktopNotifier);
|
|
19
|
+
notify(event: NotificationEvent): void;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=notification.service.d.ts.map
|
package/dist/packages/core/src/infrastructure/services/notifications/notification.service.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notification.service.d.ts","sourceRoot":"","sources":["../../../../../../../packages/core/src/infrastructure/services/notifications/notification.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EACV,iBAAiB,EAElB,MAAM,qCAAqC,CAAC;AAE7C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,8EAA8E,CAAC;AAEzH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAU7D,qBAAa,mBAAoB,YAAW,oBAAoB;IAC9D,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAkB;IACtC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAkB;gBAEtC,GAAG,EAAE,eAAe,EAAE,eAAe,EAAE,eAAe;IAKlE,MAAM,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI;CAmBvC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notification Service
|
|
3
|
+
*
|
|
4
|
+
* Implements INotificationService port interface. Fans out notification
|
|
5
|
+
* events to enabled channels:
|
|
6
|
+
* - Notification bus (for SSE → in-app toasts and browser notifications)
|
|
7
|
+
* - Desktop notifier (node-notifier for OS-level notifications)
|
|
8
|
+
*
|
|
9
|
+
* Channel enable/disable and event type filters are read from Settings.
|
|
10
|
+
*/
|
|
11
|
+
import { NotificationEventType } from '../../../domain/generated/output.js';
|
|
12
|
+
import { getSettings } from '../settings.service.js';
|
|
13
|
+
const EVENT_TYPE_TO_CONFIG_KEY = {
|
|
14
|
+
[NotificationEventType.AgentStarted]: 'agentStarted',
|
|
15
|
+
[NotificationEventType.PhaseCompleted]: 'phaseCompleted',
|
|
16
|
+
[NotificationEventType.WaitingApproval]: 'waitingApproval',
|
|
17
|
+
[NotificationEventType.AgentCompleted]: 'agentCompleted',
|
|
18
|
+
[NotificationEventType.AgentFailed]: 'agentFailed',
|
|
19
|
+
};
|
|
20
|
+
export class NotificationService {
|
|
21
|
+
bus;
|
|
22
|
+
desktopNotifier;
|
|
23
|
+
constructor(bus, desktopNotifier) {
|
|
24
|
+
this.bus = bus;
|
|
25
|
+
this.desktopNotifier = desktopNotifier;
|
|
26
|
+
}
|
|
27
|
+
notify(event) {
|
|
28
|
+
const { notifications } = getSettings();
|
|
29
|
+
// Check event type filter
|
|
30
|
+
const configKey = EVENT_TYPE_TO_CONFIG_KEY[event.eventType];
|
|
31
|
+
if (configKey && !notifications.events[configKey]) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
// Emit to bus if in-app or browser channel is enabled
|
|
35
|
+
if (notifications.inApp.enabled || notifications.browser.enabled) {
|
|
36
|
+
this.bus.emit('notification', event);
|
|
37
|
+
}
|
|
38
|
+
// Dispatch to desktop notifier if desktop channel is enabled
|
|
39
|
+
if (notifications.desktop.enabled) {
|
|
40
|
+
this.desktopNotifier.send(event.featureName, event.message);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ui.command.d.ts","sourceRoot":"","sources":["../../../../../src/presentation/cli/commands/ui.command.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,OAAO,EAAwB,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"ui.command.d.ts","sourceRoot":"","sources":["../../../../../src/presentation/cli/commands/ui.command.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,OAAO,EAAwB,MAAM,WAAW,CAAC;AAwB1D;;GAEG;AACH,wBAAgB,eAAe,IAAI,OAAO,CAkEzC"}
|
|
@@ -19,6 +19,7 @@ import { findAvailablePort, DEFAULT_PORT } from '../../../../packages/core/src/i
|
|
|
19
19
|
import { container } from '../../../../packages/core/src/infrastructure/di/container.js';
|
|
20
20
|
import { setVersionEnvVars } from '../../../../packages/core/src/infrastructure/services/version.service.js';
|
|
21
21
|
import { resolveWebDir } from '../../../../packages/core/src/infrastructure/services/web-server.service.js';
|
|
22
|
+
import { initializeNotificationWatcher, getNotificationWatcher, } from '../../../../packages/core/src/infrastructure/services/notifications/notification-watcher.service.js';
|
|
22
23
|
import { colors, fmt, messages } from '../ui/index.js';
|
|
23
24
|
function parsePort(value) {
|
|
24
25
|
const port = parseInt(value, 10);
|
|
@@ -52,6 +53,12 @@ Examples:
|
|
|
52
53
|
messages.newline();
|
|
53
54
|
const service = container.resolve('IWebServerService');
|
|
54
55
|
await service.start(port, dir, dev);
|
|
56
|
+
// Start notification watcher to detect agent status transitions
|
|
57
|
+
const runRepo = container.resolve('IAgentRunRepository');
|
|
58
|
+
const phaseTimingRepo = container.resolve('IPhaseTimingRepository');
|
|
59
|
+
const notificationService = container.resolve('INotificationService');
|
|
60
|
+
initializeNotificationWatcher(runRepo, phaseTimingRepo, notificationService);
|
|
61
|
+
getNotificationWatcher().start();
|
|
55
62
|
messages.success(`Server ready at ${fmt.code(`http://localhost:${port}`)}`);
|
|
56
63
|
messages.info('Press Ctrl+C to stop');
|
|
57
64
|
messages.newline();
|
|
@@ -67,6 +74,7 @@ Examples:
|
|
|
67
74
|
// Force exit after 5s if graceful shutdown stalls
|
|
68
75
|
const forceExit = setTimeout(() => process.exit(0), 5000);
|
|
69
76
|
forceExit.unref();
|
|
77
|
+
getNotificationWatcher().stop();
|
|
70
78
|
await service.stop();
|
|
71
79
|
process.exit(0);
|
|
72
80
|
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSE API Route: GET /api/agent-events
|
|
3
|
+
*
|
|
4
|
+
* Streams agent lifecycle notification events to connected web UI clients
|
|
5
|
+
* via Server-Sent Events (SSE). This is the first API route in the web UI.
|
|
6
|
+
*
|
|
7
|
+
* - Subscribes to the notification event bus (shared in-process singleton)
|
|
8
|
+
* - Formats events as SSE data frames (event: notification\ndata: JSON\n\n)
|
|
9
|
+
* - Sends heartbeat comments every 30 seconds to keep connection alive
|
|
10
|
+
* - Supports optional ?runId query parameter to filter events
|
|
11
|
+
* - Cleans up listeners and intervals on client disconnect
|
|
12
|
+
*/
|
|
13
|
+
export declare function GET(request: Request): Response;
|
|
14
|
+
//# sourceMappingURL=route.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../../../../src/presentation/web/app/api/agent-events/route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAkBH,wBAAgB,GAAG,CAAC,OAAO,EAAE,OAAO,GAAG,QAAQ,CA+D9C"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSE API Route: GET /api/agent-events
|
|
3
|
+
*
|
|
4
|
+
* Streams agent lifecycle notification events to connected web UI clients
|
|
5
|
+
* via Server-Sent Events (SSE). This is the first API route in the web UI.
|
|
6
|
+
*
|
|
7
|
+
* - Subscribes to the notification event bus (shared in-process singleton)
|
|
8
|
+
* - Formats events as SSE data frames (event: notification\ndata: JSON\n\n)
|
|
9
|
+
* - Sends heartbeat comments every 30 seconds to keep connection alive
|
|
10
|
+
* - Supports optional ?runId query parameter to filter events
|
|
11
|
+
* - Cleans up listeners and intervals on client disconnect
|
|
12
|
+
*/
|
|
13
|
+
import { getNotificationBus, hasNotificationBus, } from '../../../../../../packages/core/src/infrastructure/services/notifications/notification-bus.js';
|
|
14
|
+
const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
15
|
+
function formatSSEEvent(event) {
|
|
16
|
+
return `event: notification\ndata: ${JSON.stringify(event)}\n\n`;
|
|
17
|
+
}
|
|
18
|
+
function formatHeartbeat() {
|
|
19
|
+
return ': heartbeat\n\n';
|
|
20
|
+
}
|
|
21
|
+
export function GET(request) {
|
|
22
|
+
const url = new URL(request.url);
|
|
23
|
+
const runIdFilter = url.searchParams.get('runId');
|
|
24
|
+
const stream = new ReadableStream({
|
|
25
|
+
start(controller) {
|
|
26
|
+
const encoder = new TextEncoder();
|
|
27
|
+
// Check if bus is available
|
|
28
|
+
if (!hasNotificationBus()) {
|
|
29
|
+
controller.close();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const bus = getNotificationBus();
|
|
33
|
+
// Notification event listener
|
|
34
|
+
const onNotification = (event) => {
|
|
35
|
+
// Apply runId filter if set
|
|
36
|
+
if (runIdFilter && event.agentRunId !== runIdFilter) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
controller.enqueue(encoder.encode(formatSSEEvent(event)));
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Stream may be closed — ignore enqueue errors
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
bus.on('notification', onNotification);
|
|
47
|
+
// Heartbeat to keep connection alive
|
|
48
|
+
const heartbeatInterval = setInterval(() => {
|
|
49
|
+
try {
|
|
50
|
+
controller.enqueue(encoder.encode(formatHeartbeat()));
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Stream may be closed — ignore enqueue errors
|
|
54
|
+
}
|
|
55
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
56
|
+
// Cleanup on client disconnect
|
|
57
|
+
const cleanup = () => {
|
|
58
|
+
bus.removeListener('notification', onNotification);
|
|
59
|
+
clearInterval(heartbeatInterval);
|
|
60
|
+
try {
|
|
61
|
+
controller.close();
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// Stream may already be closed
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
request.signal.addEventListener('abort', cleanup, { once: true });
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
return new Response(stream, {
|
|
71
|
+
headers: {
|
|
72
|
+
'Content-Type': 'text/event-stream',
|
|
73
|
+
'Cache-Control': 'no-cache',
|
|
74
|
+
Connection: 'keep-alive',
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"page.d.ts","sourceRoot":"","sources":["../../../../../src/presentation/web/app/page.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"page.d.ts","sourceRoot":"","sources":["../../../../../src/presentation/web/app/page.tsx"],"names":[],"mappings":"AAWA,6EAA6E;AAC7E,eAAO,MAAM,OAAO,kBAAkB,CAAC;AAqBvC,wBAA8B,QAAQ,qDAmFrC"}
|