@x-otto/notification 0.0.1-alpha.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/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # @x-otto/notification
2
+
3
+ > Unified notification spine for otto sessions: terminal escapes, external commands, and app-internal callbacks.
4
+
5
+ `@x-otto/notification` delivers session-level notifications (turn complete, errors, approval requests) through configurable channels. It runs in CLI, TUI, and headless modes with zero frontend dependencies. Notifications are fire-and-forget — they never block or interrupt the session.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pnpm add @x-otto/notification
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```typescript
16
+ import {
17
+ NotificationDispatcher,
18
+ TerminalChannel,
19
+ PresenceTracker,
20
+ TimestampPresenceGate,
21
+ } from '@x-otto/notification'
22
+
23
+ const dispatcher = new NotificationDispatcher({
24
+ getConfig: () => settings.notification,
25
+ gate: new TimestampPresenceGate({
26
+ state: presenceTracker,
27
+ getCondition: () => 'unfocused',
28
+ getIdleThresholdMs: () => 60000,
29
+ }),
30
+ })
31
+
32
+ // Register terminal channel (OSC 9/99/777 + bell)
33
+ dispatcher.registerChannel(new TerminalChannel({
34
+ getConfig: () => settings.notification,
35
+ }))
36
+
37
+ // Subscribe to a session's EventBus
38
+ dispatcher.attach(session)
39
+ ```
40
+
41
+ The dispatcher automatically normalizes session events to four kinds:
42
+ - `turn_complete` — agent finished responding
43
+ - `error` — session error
44
+ - `approval_required` — user approval needed for tool/plan
45
+ - `input_required` — user input needed
46
+
47
+ Presence gating suppresses notifications when the user is actively interacting (idle < threshold), and only the highest-priority event per tick is delivered.
48
+
49
+ ## Key Exports
50
+
51
+ ### Dispatcher
52
+ - `NotificationDispatcher` — Subscribe to sessions, merge events, fan-out to registered channels
53
+ - `NotificationDispatcherOptions` — `getConfig`, `gate`, `schedule`, `isFollowUpPending`
54
+
55
+ ### Channels
56
+ - `TerminalChannel` — Terminal escape sequences (OSC 9 for iTerm2, OSC 99 for Kitty, OSC 777 for Ghostty) + bell fallback
57
+ - `CallbackChannel` — Generic callback bridge for app-internal or hook dispatch
58
+ - `CommandChannel` — Spawn external program, JSON payload on stdin (trust-gated)
59
+ - `MemoryChannel` — In-memory accumulator for testing
60
+
61
+ ### Gating
62
+ - `PresenceTracker` — Idle time tracker (records interactions, optional DEC1004 focus)
63
+ - `TimestampPresenceGate` — Idle-threshold-based gate (60s turn_complete, 6s urgent)
64
+ - `ALWAYS_PRESENT_GATE` — Bypass gate (always deliver)
65
+
66
+ ### Terminal
67
+ - `detectTerminal(env)` — Probe terminal capabilities
68
+ - `bell()` — ASCII bell character
69
+ - `osc9()` / `osc99()` / `osc777()` — Desktop notification escape sequences
70
+
71
+ ## Dependencies
72
+
73
+ - **Internal**: `@x-otto/hook-contracts` (type-only), `@x-otto/setting` (type-only)
74
+ - **External**: Node.js built-ins only
75
+
76
+ ## Related
77
+
78
+ - [Architecture](./ARCHITECTURE.md)
@@ -0,0 +1,313 @@
1
+ import { AgentSessionEvent } from "@x-otto/hook-contracts";
2
+ import { NotificationConfig } from "@x-otto/setting";
3
+
4
+ //#region src/types.d.ts
5
+ /**
6
+ * 归一后的通知类别。来自 AgentSession EventBus 的多种事件被映射到这四类之一
7
+ * (RFC-047 §6.3)。`plan.approval.required` 也归入 `approval_required`。
8
+ */
9
+ type NotificationKind = 'turn_complete' | 'error' | 'approval_required' | 'input_required';
10
+ /**
11
+ * 交付给带内/带外通道的 wire 形状。**冻结契约**(RFC-047 D4)——一旦发布,
12
+ * 字段改动 = 破坏性变更,须更新 golden 测试(见 tests/payload-golden.test.ts)。
13
+ * 带外 hook/程序消费此对象(JSON → 子进程 stdin + 尾换行,OQ2)。
14
+ */
15
+ interface NotificationPayload {
16
+ type: NotificationKind;
17
+ title?: string;
18
+ message: string;
19
+ sessionId: string;
20
+ cwd: string;
21
+ /** 会话转录路径,省得外部程序自己解析(沿用 CC `transcript_path` 语义)。 */
22
+ transcriptPath?: string;
23
+ /** turn_complete 时带最后一条助手消息文本,外部程序无需读转录。 */
24
+ lastAssistantMessage?: string;
25
+ }
26
+ /**
27
+ * 交付通道接口。**transport-agnostic**(RFC-047 OQ4)——不嵌任何终端假设,
28
+ * 终端/应用内/带外/未来 web 通道都实现此接口并注册进 dispatcher。
29
+ */
30
+ interface NotificationChannel {
31
+ readonly id: string;
32
+ /** 当前环境下该通道是否可用(如终端不支持 OSC、未配 command、测试环境静音)。 */
33
+ isAvailable(): boolean;
34
+ deliver(payload: NotificationPayload): void | Promise<void>;
35
+ }
36
+ /**
37
+ * dispatcher 订阅的最小会话面。`AgentSession`(extends EventBus)结构上满足此型,
38
+ * 故 @x-otto/notification 无需依赖 @x-otto/runtime(保持前端无关纯层,RFC-047 §6.1)。
39
+ */
40
+ interface NotifiableSession {
41
+ readonly id: string;
42
+ subscribe(callback: (event: AgentSessionEvent) => void): () => void;
43
+ }
44
+ /** 带内终端通道选择(RFC-047 §6.4)——从 setting 的 channel 字段派生,不再重复枚举。 */
45
+ type NotificationChannelName = NonNullable<NotificationConfig['channel']>;
46
+ /**
47
+ * 单槽合并的优先级:数值越大越优先(busy turn 内多事件只留最高优先级那条,RFC-047 §2.1)。
48
+ * approval/input(需要人立刻处理)> error > turn_complete。
49
+ */
50
+ declare const NOTIFICATION_PRIORITY: Record<NotificationKind, number>;
51
+ //#endregion
52
+ //#region src/gating.d.ts
53
+ /**
54
+ * 在场门控(RFC-047 D5):判断"此刻是否该打扰用户"。
55
+ *
56
+ * 两种在场信号,按可得性择优:
57
+ * - **时间戳在场(默认,无需终端 API)**:距上次用户交互的空闲时长。交互信号默认取自
58
+ * `prompt.start`(用户刚提交)——故 turn_complete 时 idleMs≈本回合时长:回合够长(≥阈值)
59
+ * 说明用户多半已离开 → 提醒;回合很快说明用户还在盯 → 不扰。TUI 可选喂更细的按键时间。
60
+ * - **DEC1004 焦点(opt-in `OTTO_TUI_FOCUS=1`)**:终端聚焦=在场。已知焦点时优先用它(焦点模型)。
61
+ *
62
+ * 两档阈值(6s/60s):turn_complete 用 idle_threshold_ms(默认 60s);
63
+ * 紧急类(error/approval/input)用 dialogThresholdMs(默认 6s)——用户刚操作过就别响铃,否则提醒。
64
+ */
65
+ interface PresenceState {
66
+ /** 距上次用户交互的毫秒数(从未交互返回 Infinity)。 */
67
+ idleMs(): number;
68
+ /** 终端焦点;DEC1004 跟踪未开启时返回 undefined(→ 退回时间戳模型)。 */
69
+ isFocused?(): boolean | undefined;
70
+ }
71
+ interface PresenceGateOptions {
72
+ getCondition: () => 'unfocused' | 'always' | undefined;
73
+ /** turn_complete 阈值(默认 60000)。 */
74
+ getIdleThresholdMs: () => number | undefined;
75
+ /** 紧急类阈值(默认 6000)。 */
76
+ dialogThresholdMs?: number;
77
+ state: PresenceState;
78
+ }
79
+ interface PresenceGate {
80
+ shouldNotify(kind: NotificationKind): boolean;
81
+ }
82
+ declare class TimestampPresenceGate implements PresenceGate {
83
+ private readonly getCondition;
84
+ private readonly getIdleThresholdMs;
85
+ private readonly dialogThresholdMs;
86
+ private readonly state;
87
+ constructor(options: PresenceGateOptions);
88
+ shouldNotify(kind: NotificationKind): boolean;
89
+ }
90
+ /** 默认门控:永远交付。脊柱无 PresenceGate 时的兜底(如纯单测)。 */
91
+ declare const ALWAYS_PRESENT_GATE: PresenceGate;
92
+ //#endregion
93
+ //#region src/presence-tracker.d.ts
94
+ interface PresenceTrackerOptions {
95
+ /** 时钟(可注入便于测试)。默认 Date.now。 */
96
+ now?: () => number;
97
+ /** 是否启用焦点跟踪(DEC1004 opt-in)。关闭时 isFocused 恒返回 undefined → 时间戳模型。 */
98
+ focusTracking?: boolean;
99
+ }
100
+ /**
101
+ * 在场状态的具体实现(RFC-047 N3)。脊柱(App)持有一份;交互/焦点信号由上游喂入:
102
+ * - dispatcher 在 `prompt.start` 调 recordInteraction(默认、零 TUI 耦合)。
103
+ * - TUI 可选在每次按键调 recordInteraction(更细)、DEC1004 焦点事件调 setFocus(opt-in)。
104
+ */
105
+ declare class PresenceTracker implements PresenceState {
106
+ private lastInteractionAt;
107
+ private focus;
108
+ private readonly now;
109
+ constructor(options?: PresenceTrackerOptions);
110
+ recordInteraction(): void;
111
+ setFocus(focused: boolean): void;
112
+ idleMs(): number;
113
+ isFocused(): boolean | undefined;
114
+ }
115
+ //#endregion
116
+ //#region src/dispatcher.d.ts
117
+ interface NotificationDispatcherOptions {
118
+ /** 惰性读配置:settings 可 live-reload,故每次归一时取最新(RFC-047 §6.1 惰性门)。 */
119
+ getConfig: () => NotificationConfig | undefined;
120
+ /** 在场门控(RFC-047 D5)。默认 always-present;N3 注入真实时间戳/焦点门。 */
121
+ gate?: PresenceGate;
122
+ /** payload.cwd 解析。默认 process.cwd(AgentSessionEvent 不携 cwd)。 */
123
+ getCwd?: () => string;
124
+ /**
125
+ * 合并后的 flush 调度器。默认 queueMicrotask——同一同步 tick 内多事件先合并再交付一次
126
+ * (busy turn 防刷屏,RFC-047 §2.1)。测试可注入捕获式调度器手动 flush。
127
+ */
128
+ schedule?: (flush: () => void) => void;
129
+ /** 通道交付出错回调。默认吞掉——通知绝不冒泡破坏会话(fire-and-forget)。 */
130
+ onError?: (channelId: string, error: unknown) => void;
131
+ /** 用户活动回调(RFC-047 N3):dispatcher 在 prompt.start 调用,用于喂在场跟踪器。 */
132
+ onInteraction?: () => void;
133
+ /** RFC-047 R4:有待跑 follow-up 则抑制完成通知(TUI 经 setFollowUpPendingProbe 注入读 state.queued)。 */
134
+ isFollowUpPending?: () => boolean;
135
+ }
136
+ /**
137
+ * 通知脊柱(RFC-047 §6)。订阅 AgentSession EventBus 的触发事件 → 归一 →
138
+ * 配置过滤 + 在场门控 → 单槽优先级合并 → 扇出到注册的交付通道。
139
+ *
140
+ * 前端无关:通过 NotifiableSession 结构型订阅、NotificationChannel 注册式交付,
141
+ * 不依赖 runtime/tui。检测走 EventBus 单 substrate(RFC-047 D1);hook/终端/应用内
142
+ * 通道由各前端注入注册(D2)。
143
+ */
144
+ declare class NotificationDispatcher {
145
+ private readonly channels;
146
+ private readonly subscriptions;
147
+ private readonly getConfig;
148
+ private readonly gate;
149
+ private readonly getCwd;
150
+ private readonly schedule;
151
+ private readonly onError;
152
+ private readonly onInteraction;
153
+ private readonly isFollowUpPending;
154
+ private pending;
155
+ private flushScheduled;
156
+ /** 每会话累积的最后一段助手文本(喂 turn_complete 的 message/lastAssistantMessage)。 */
157
+ private readonly lastAssistantText;
158
+ constructor(options: NotificationDispatcherOptions);
159
+ /** 注册交付通道(D2:新增能力=注册,不改 dispatcher)。 */
160
+ registerChannel(channel: NotificationChannel): void;
161
+ /** 订阅一个会话的 EventBus(在 app.createSession 构造层调用,全模式统一,D1)。 */
162
+ attach(session: NotifiableSession): void;
163
+ /** 取消订阅(会话销毁时)。 */
164
+ detach(sessionId: string): void;
165
+ /** 释放全部订阅。 */
166
+ dispose(): void;
167
+ /** 测试用:立即同步 flush 当前合并槽。 */
168
+ flushNow(): void;
169
+ private handleEvent;
170
+ private normalize;
171
+ /** 单槽合并:同 tick 内只保留最高(同优先级取最新)优先级那条,再调度一次 flush。 */
172
+ private enqueue;
173
+ private flush;
174
+ }
175
+ //#endregion
176
+ //#region src/channels/memory-channel.d.ts
177
+ /**
178
+ * 内存收集通道:把交付的 payload 累积到数组。用作脊柱自测的假通道(RFC-047 N1-06),
179
+ * 也是 NotificationChannel 接口的最小参考实现。生产交付通道(终端/带外/应用内)见 N2/N4。
180
+ */
181
+ declare class MemoryChannel implements NotificationChannel {
182
+ readonly id: string;
183
+ readonly delivered: NotificationPayload[];
184
+ private available;
185
+ constructor(id?: string, available?: boolean);
186
+ isAvailable(): boolean;
187
+ setAvailable(available: boolean): void;
188
+ deliver(payload: NotificationPayload): void;
189
+ }
190
+ //#endregion
191
+ //#region src/channels/terminal-channel.d.ts
192
+ interface TerminalChannelOptions {
193
+ /** 惰性读通知配置(channel 选择 + sound)。 */
194
+ getConfig: () => NotificationConfig | undefined;
195
+ /** escape 写出。默认裸 process.stdout.write(不可见控制字节,安全,不走 Ink,N1 研究实证)。 */
196
+ write?: (data: string) => void;
197
+ /** 是否 TTY(非 TTY=管道/headless 输出,不发 escape 避免污染)。 */
198
+ isTty?: () => boolean;
199
+ /** 终端能力探测的 env 源。 */
200
+ env?: Record<string, string | undefined>;
201
+ /** 测试静音(规则8)。默认 NODE_ENV==='test'。 */
202
+ isTest?: () => boolean;
203
+ }
204
+ /**
205
+ * 带内终端通道(RFC-047 N2):把通知写成终端 escape(响铃 / iTerm2·Kitty·Ghostty 桌面通知)。
206
+ * 纯 stdout escape,无 Ink/React 依赖 → 住在 @x-otto/notification,可被 CLI/TUI/headless 统一复用
207
+ * (App 默认注册;非 TTY 自动失活,故管道/headless 输出不被污染)。
208
+ */
209
+ declare class TerminalChannel implements NotificationChannel {
210
+ readonly id = "terminal";
211
+ private readonly getConfig;
212
+ private readonly write;
213
+ private readonly isTty;
214
+ private readonly env;
215
+ private readonly isTest;
216
+ constructor(options: TerminalChannelOptions);
217
+ isAvailable(): boolean;
218
+ deliver(payload: NotificationPayload): void;
219
+ }
220
+ //#endregion
221
+ //#region src/channels/callback-channel.d.ts
222
+ /**
223
+ * 通用回调通道(RFC-047 N4):deliver 时调注入的回调。用于把脊柱接到外部机制而不引入依赖——
224
+ * 如 HookChannel(回调 = `hookRegistry.emit('notification', ...)`,由 coding 注入)、
225
+ * 应用内通道(回调 = `handle.pushNotification(...)`,由 CLI/TUI 注入)。
226
+ */
227
+ declare class CallbackChannel implements NotificationChannel {
228
+ readonly id: string;
229
+ private readonly available;
230
+ private readonly callback;
231
+ constructor(id: string, available: () => boolean, callback: (payload: NotificationPayload) => void);
232
+ isAvailable(): boolean;
233
+ deliver(payload: NotificationPayload): void;
234
+ }
235
+ //#endregion
236
+ //#region src/channels/command-channel.d.ts
237
+ interface WritableLike {
238
+ write(data: string): void;
239
+ end(): void;
240
+ on?(event: 'error', listener: () => void): void;
241
+ }
242
+ interface ChildLike {
243
+ stdin: WritableLike | null;
244
+ on?(event: 'error', listener: () => void): void;
245
+ unref?(): void;
246
+ }
247
+ type SpawnLike = (command: string, args: string[]) => ChildLike;
248
+ interface CommandChannelOptions {
249
+ getConfig: () => NotificationConfig | undefined;
250
+ /** 信任门(D6/规则2):项目级配置可来自克隆仓 → 未受信不 spawn,杜绝 RCE。 */
251
+ isTrusted: () => boolean;
252
+ /** 可注入 spawn(测试)。默认 node child_process,argv 不过 shell(无插值=无 RCE 面)。 */
253
+ spawn?: SpawnLike;
254
+ /** 测试静音(规则8)。默认 NODE_ENV==='test'。 */
255
+ isTest?: () => boolean;
256
+ }
257
+ /**
258
+ * 带外外部程序通道(RFC-047 N4,`notify=[argv]` 模型):把通知 payload 作为 JSON 写入
259
+ * 用户配置命令的 stdin(尾换行,OQ2),fire-and-forget,stdio 隔离,错误吞掉——绝不阻塞会话。
260
+ * argv 直接 spawn 不过 shell(无引号/插值/RCE 面)。受信门控(项目可来自克隆仓)。
261
+ */
262
+ declare class CommandChannel implements NotificationChannel {
263
+ readonly id = "command";
264
+ private readonly getConfig;
265
+ private readonly isTrusted;
266
+ private readonly spawn;
267
+ private readonly isTest;
268
+ constructor(options: CommandChannelOptions);
269
+ isAvailable(): boolean;
270
+ deliver(payload: NotificationPayload): void;
271
+ }
272
+ //#endregion
273
+ //#region src/terminal/osc.d.ts
274
+ /** OSC terminal notifications: OSC 9 (progress) / OSC 777 (alert) sequence builders. */
275
+ type Multiplexer = 'none' | 'tmux' | 'screen';
276
+ /** 终端响铃。裸 BEL——tmux 须看见原始 BEL 才置 window 活动标志,故**永不**包 DCS(规则4)。 */
277
+ declare function bell(): string;
278
+ /** iTerm2 OSC 9 通知。 */
279
+ declare function osc9(message: string): string;
280
+ /** Kitty OSC 99 通知(title + body 两段;i=会话内去重 id,d=0 不替换,p 段类型)。 */
281
+ declare function osc99(title: string, message: string, id?: string): string;
282
+ /** Ghostty OSC 777 通知。 */
283
+ declare function osc777(title: string, message: string): string;
284
+ /**
285
+ * multiplexer DCS 透传包装(仅 OSC 9/99/777,**不含 BEL**)。
286
+ * tmux:`\x1bPtmux;<内部 ESC 加倍>\x1b\\`;screen:`\x1bP<seq>\x1b\\`。
287
+ * 需 tmux `set -g allow-passthrough on`。
288
+ */
289
+ declare function wrapForMultiplexer(seq: string, mux: Multiplexer): string;
290
+ //#endregion
291
+ //#region src/terminal/detect.d.ts
292
+ /** `auto` 能解析到的具体通道(排除 auto/disabled)。 */
293
+ type ResolvedChannel = Exclude<NotificationChannelName, 'auto' | 'disabled'>;
294
+ interface TerminalCapabilities {
295
+ /** 终端身份(调试用)。 */
296
+ program: string;
297
+ multiplexer: Multiplexer;
298
+ /** `auto` 选择器解析出的通道(按终端能力)。 */
299
+ autoChannel: ResolvedChannel;
300
+ }
301
+ type Env = Record<string, string | undefined>;
302
+ /**
303
+ * 终端能力探测(RFC-047 N2-02)。`auto` 通道据此选具体 escape:
304
+ * iTerm2 → OSC 9 / kitty → OSC 99 / ghostty → OSC 777 /
305
+ * WezTerm·Warp → OSC 9(实证支持)/ 其余(含 Apple_Terminal)→ 响铃兜底。
306
+ * 未知终端兜底 terminal_bell——BEL 通用安全,胜过盲发可能乱码的 OSC 或彻底无声。
307
+ */
308
+ declare function detectTerminal(env?: Env): TerminalCapabilities;
309
+ /** multiplexer 探测(决定 OSC 9/99/777 是否 DCS 透传;与 tui sync-output 的 tmux 判定关注点不同)。 */
310
+ declare function detectMultiplexer(env?: Env): Multiplexer;
311
+ //#endregion
312
+ export { ALWAYS_PRESENT_GATE, CallbackChannel, CommandChannel, type CommandChannelOptions, MemoryChannel, type Multiplexer, NOTIFICATION_PRIORITY, type NotifiableSession, type NotificationChannel, type NotificationChannelName, type NotificationConfig, NotificationDispatcher, type NotificationDispatcherOptions, type NotificationKind, type NotificationPayload, type PresenceGate, type PresenceGateOptions, type PresenceState, PresenceTracker, type PresenceTrackerOptions, type ResolvedChannel, type SpawnLike, type TerminalCapabilities, TerminalChannel, type TerminalChannelOptions, TimestampPresenceGate, bell, detectMultiplexer, detectTerminal, osc777, osc9, osc99, wrapForMultiplexer };
313
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/types.ts","../src/gating.ts","../src/presence-tracker.ts","../src/dispatcher.ts","../src/channels/memory-channel.ts","../src/channels/terminal-channel.ts","../src/channels/callback-channel.ts","../src/channels/command-channel.ts","../src/terminal/osc.ts","../src/terminal/detect.ts"],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ import{spawn as e}from"node:child_process";const t={approval_required:3,input_required:3,error:2,turn_complete:1};var n=class{getCondition;getIdleThresholdMs;dialogThresholdMs;state;constructor(e){this.getCondition=e.getCondition,this.getIdleThresholdMs=e.getIdleThresholdMs,this.dialogThresholdMs=e.dialogThresholdMs??6e3,this.state=e.state}shouldNotify(e){if((this.getCondition()??`unfocused`)===`always`)return!0;let t=this.state.isFocused?.();if(t!==void 0)return!t;let n=e===`turn_complete`?this.getIdleThresholdMs()??6e4:this.dialogThresholdMs;return this.state.idleMs()>=n}};const r={shouldNotify:()=>!0};var i=class{lastInteractionAt;focus;now;constructor(e={}){this.now=e.now??(()=>Date.now()),this.lastInteractionAt=this.now(),this.focus=e.focusTracking?!0:void 0}recordInteraction(){this.lastInteractionAt=this.now()}setFocus(e){this.focus=e}idleMs(){return this.now()-this.lastInteractionAt}isFocused(){return this.focus}},a=class{channels=[];subscriptions=new Map;getConfig;gate;getCwd;schedule;onError;onInteraction;isFollowUpPending;pending=null;flushScheduled=!1;lastAssistantText=new Map;constructor(e){this.getConfig=e.getConfig,this.gate=e.gate??r,this.getCwd=e.getCwd??(()=>process.cwd()),this.schedule=e.schedule??(e=>queueMicrotask(e)),this.onError=e.onError??(()=>{}),this.onInteraction=e.onInteraction??(()=>{}),this.isFollowUpPending=e.isFollowUpPending??(()=>!1)}registerChannel(e){this.channels.push(e)}attach(e){this.detach(e.id);let t=e.subscribe(t=>this.handleEvent(t,e));this.subscriptions.set(e.id,t)}detach(e){let t=this.subscriptions.get(e);t&&(t(),this.subscriptions.delete(e)),this.lastAssistantText.delete(e)}dispose(){for(let e of this.subscriptions.values())e();this.subscriptions.clear(),this.lastAssistantText.clear(),this.pending=null}flushNow(){this.flush()}handleEvent(e,t){if(e.type===`prompt.start`){this.lastAssistantText.delete(t.id),this.onInteraction();return}if(e.type===`stream.event`){let n=e.event;n.type===`text_end`&&n.content.trim()&&this.lastAssistantText.set(t.id,n.content);return}let n=this.normalize(e,t);n&&this.enqueue(n)}normalize(e,t){let n=this.getConfig()??{};if(n.enabled===!1)return null;let r=o(e);if(!r||!s(r,n)||!this.gate.shouldNotify(r)||r===`turn_complete`&&this.isFollowUpPending())return null;let i=r===`turn_complete`?this.lastAssistantText.get(t.id):void 0;r===`turn_complete`&&this.lastAssistantText.delete(t.id);let{title:a,message:c}=l(r,e,i);return{type:r,title:a,message:c,sessionId:t.id,cwd:this.getCwd(),...i?{lastAssistantMessage:i}:{}}}enqueue(e){if((this.pending===null||t[e.type]>=t[this.pending.type])&&(this.pending=e),!this.flushScheduled){this.flushScheduled=!0;try{this.schedule(()=>this.flush())}catch(e){throw this.flushScheduled=!1,e}}}flush(){this.flushScheduled=!1;let e=this.pending;if(this.pending=null,e){for(let t of this.channels)if(t.isAvailable())try{let n=t.deliver(e);n instanceof Promise&&n.catch(e=>this.onError(t.id,e))}catch(e){this.onError(t.id,e)}}}};function o(e){switch(e.type){case`prompt.end`:return`turn_complete`;case`error`:return`error`;case`approval.required`:case`plan.approval.required`:return`approval_required`;case`ask.user.required`:return`input_required`;default:return null}}function s(e,t){switch(e){case`turn_complete`:return t.on_completion!==!1||t.on_idle===!0;case`error`:return t.on_error!==!1;case`approval_required`:case`input_required`:return!0}}function c(e,t=120){let n=e.replace(/\s+/g,` `).trim();return n.length>t?`${n.slice(0,t-1)}…`:n}function l(e,t,n){switch(e){case`turn_complete`:return{title:`otto`,message:n?c(n):`Turn complete — otto finished responding`};case`error`:return{title:`otto error`,message:t.type===`error`?t.error instanceof Error?t.error.message:String(t.error??`Session error`):`Session error`};case`approval_required`:return t.type===`approval.required`?{title:`Approval required`,message:`${t.toolName} needs your approval`}:{title:`Approval required`,message:`A plan needs your approval`};case`input_required`:return{title:`Input required`,message:t.type===`ask.user.required`?t.question.question:`otto needs your input`}}}var u=class{id;delivered=[];available;constructor(e=`memory`,t=!0){this.id=e,this.available=t}isAvailable(){return this.available}setAvailable(e){this.available=e}deliver(e){this.delivered.push(e)}};function d(e){let t=``;for(let n of e){let e=n.codePointAt(0)??0;t+=e<32||e>=127&&e<=159?` `:n}return t}function f(){return`\x07`}function p(e){return`]9;${d(e)}`}function m(e,t,n=`1`){return`]99;i=${n}:d=0:p=title;${d(e)}\\]99;i=${n}:d=1:p=body;${d(t)}\\`}function h(e,t){return`]777;notify;${d(e)};${d(t)}`}function g(e,t){return t===`tmux`?`Ptmux;${e.replace(/\x1b/g,`\x1B\x1B`)}\\`:t===`screen`?`P${e}\\`:e}function _(e=process.env){let t=(e.TERM_PROGRAM??``).toLowerCase(),n=(e.TERM??``).toLowerCase(),r=`terminal_bell`;return t===`iterm.app`||t===`wezterm`||t===`warpterminal`?r=`iterm2`:n.includes(`kitty`)||e.KITTY_WINDOW_ID?r=`kitty`:(t===`ghostty`||e.GHOSTTY_RESOURCES_DIR)&&(r=`ghostty`),{program:t||n||`unknown`,multiplexer:v(e),autoChannel:r}}function v(e=process.env){return e.TMUX?`tmux`:e.STY||(e.TERM??``).startsWith(`screen`)?`screen`:`none`}var y=class{id=`terminal`;getConfig;write;isTty;env;isTest;constructor(e){this.getConfig=e.getConfig,this.write=e.write??(e=>void process.stdout.write(e)),this.isTty=e.isTty??(()=>!!process.stdout.isTTY),this.env=e.env??process.env,this.isTest=e.isTest??(()=>process.env.NODE_ENV===`test`)}isAvailable(){return this.isTest()||!this.isTty()?!1:(this.getConfig()?.channel??`auto`)!==`disabled`}deliver(e){let t=this.getConfig()??{},n=t.channel??`auto`;if(n===`disabled`)return;let r=_(this.env),i=b(n===`auto`?r.autoChannel:n,e,r.multiplexer,!!t.sound);i&&this.write(i)}};function b(e,t,n,r){let i=t.title??`otto`;if(e===`terminal_bell`)return f();let a;switch(e){case`iterm2`:case`iterm2_with_bell`:a=p(`${i}: ${t.message}`);break;case`kitty`:a=m(i,t.message);break;case`ghostty`:a=h(i,t.message);break}let o=g(a,n);return(r||e===`iterm2_with_bell`)&&(o+=f()),o}var x=class{constructor(e,t,n){this.id=e,this.available=t,this.callback=n}isAvailable(){return this.available()}deliver(e){this.callback(e)}},S=class{id=`command`;getConfig;isTrusted;spawn;isTest;constructor(e){this.getConfig=e.getConfig,this.isTrusted=e.isTrusted,this.spawn=e.spawn??C,this.isTest=e.isTest??(()=>process.env.NODE_ENV===`test`)}isAvailable(){if(this.isTest())return!1;let e=this.getConfig()?.command;return!!(e&&e.length>0)&&this.isTrusted()}deliver(e){let t=this.getConfig()?.command;if(!t||t.length===0||this.isTest()||!this.isTrusted())return;let[n,...r]=t;if(n!==void 0)try{let t=this.spawn(n,r);t.on?.(`error`,()=>{}),t.stdin?.on?.(`error`,()=>{}),t.stdin?.write(`${JSON.stringify(e)}\n`),t.stdin?.end(),t.unref?.()}catch{}}};const C=(t,n)=>e(t,n,{stdio:[`pipe`,`ignore`,`ignore`]});export{r as ALWAYS_PRESENT_GATE,x as CallbackChannel,S as CommandChannel,u as MemoryChannel,t as NOTIFICATION_PRIORITY,a as NotificationDispatcher,i as PresenceTracker,y as TerminalChannel,n as TimestampPresenceGate,f as bell,v as detectMultiplexer,_ as detectTerminal,h as osc777,p as osc9,m as osc99,g as wrapForMultiplexer};
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":["nodeSpawn"],"sources":["../src/types.ts","../src/gating.ts","../src/presence-tracker.ts","../src/dispatcher.ts","../src/channels/memory-channel.ts","../src/terminal/osc.ts","../src/terminal/detect.ts","../src/channels/terminal-channel.ts","../src/channels/callback-channel.ts","../src/channels/command-channel.ts"],"sourcesContent":["import type { AgentSessionEvent } from '@x-otto/hook-contracts'\n\n/**\n * 归一后的通知类别。来自 AgentSession EventBus 的多种事件被映射到这四类之一\n * (RFC-047 §6.3)。`plan.approval.required` 也归入 `approval_required`。\n */\nexport type NotificationKind = 'turn_complete' | 'error' | 'approval_required' | 'input_required'\n\n/**\n * 交付给带内/带外通道的 wire 形状。**冻结契约**(RFC-047 D4)——一旦发布,\n * 字段改动 = 破坏性变更,须更新 golden 测试(见 tests/payload-golden.test.ts)。\n * 带外 hook/程序消费此对象(JSON → 子进程 stdin + 尾换行,OQ2)。\n */\nexport interface NotificationPayload {\n type: NotificationKind\n title?: string\n message: string\n sessionId: string\n cwd: string\n /** 会话转录路径,省得外部程序自己解析(沿用 CC `transcript_path` 语义)。 */\n transcriptPath?: string\n /** turn_complete 时带最后一条助手消息文本,外部程序无需读转录。 */\n lastAssistantMessage?: string\n}\n\n/**\n * 交付通道接口。**transport-agnostic**(RFC-047 OQ4)——不嵌任何终端假设,\n * 终端/应用内/带外/未来 web 通道都实现此接口并注册进 dispatcher。\n */\nexport interface NotificationChannel {\n readonly id: string\n /** 当前环境下该通道是否可用(如终端不支持 OSC、未配 command、测试环境静音)。 */\n isAvailable(): boolean\n deliver(payload: NotificationPayload): void | Promise<void>\n}\n\n/**\n * dispatcher 订阅的最小会话面。`AgentSession`(extends EventBus)结构上满足此型,\n * 故 @x-otto/notification 无需依赖 @x-otto/runtime(保持前端无关纯层,RFC-047 §6.1)。\n */\nexport interface NotifiableSession {\n readonly id: string\n subscribe(callback: (event: AgentSessionEvent) => void): () => void\n}\n\n/**\n * dispatcher 读取的通知配置——单一真源在 @x-otto/setting(`NotificationConfig` 的 zod schema)。\n * 此处直接消费该类型(type-only,零运行时耦合),不再手写结构子集镜像(消 mirror-debt)。\n */\nimport type { NotificationConfig } from '@x-otto/setting'\nexport type { NotificationConfig }\n\n/** 带内终端通道选择(RFC-047 §6.4)——从 setting 的 channel 字段派生,不再重复枚举。 */\nexport type NotificationChannelName = NonNullable<NotificationConfig['channel']>\n\n/**\n * 单槽合并的优先级:数值越大越优先(busy turn 内多事件只留最高优先级那条,RFC-047 §2.1)。\n * approval/input(需要人立刻处理)> error > turn_complete。\n */\nexport const NOTIFICATION_PRIORITY: Record<NotificationKind, number> = {\n approval_required: 3,\n input_required: 3,\n error: 2,\n turn_complete: 1,\n}\n","import type { NotificationKind } from './types'\n\n/**\n * 在场门控(RFC-047 D5):判断\"此刻是否该打扰用户\"。\n *\n * 两种在场信号,按可得性择优:\n * - **时间戳在场(默认,无需终端 API)**:距上次用户交互的空闲时长。交互信号默认取自\n * `prompt.start`(用户刚提交)——故 turn_complete 时 idleMs≈本回合时长:回合够长(≥阈值)\n * 说明用户多半已离开 → 提醒;回合很快说明用户还在盯 → 不扰。TUI 可选喂更细的按键时间。\n * - **DEC1004 焦点(opt-in `OTTO_TUI_FOCUS=1`)**:终端聚焦=在场。已知焦点时优先用它(焦点模型)。\n *\n * 两档阈值(6s/60s):turn_complete 用 idle_threshold_ms(默认 60s);\n * 紧急类(error/approval/input)用 dialogThresholdMs(默认 6s)——用户刚操作过就别响铃,否则提醒。\n */\nexport interface PresenceState {\n /** 距上次用户交互的毫秒数(从未交互返回 Infinity)。 */\n idleMs(): number\n /** 终端焦点;DEC1004 跟踪未开启时返回 undefined(→ 退回时间戳模型)。 */\n isFocused?(): boolean | undefined\n}\n\nexport interface PresenceGateOptions {\n getCondition: () => 'unfocused' | 'always' | undefined\n /** turn_complete 阈值(默认 60000)。 */\n getIdleThresholdMs: () => number | undefined\n /** 紧急类阈值(默认 6000)。 */\n dialogThresholdMs?: number\n state: PresenceState\n}\n\nexport interface PresenceGate {\n shouldNotify(kind: NotificationKind): boolean\n}\n\nconst DEFAULT_IDLE_THRESHOLD_MS = 60_000\nconst DEFAULT_DIALOG_THRESHOLD_MS = 6_000\n\nexport class TimestampPresenceGate implements PresenceGate {\n private readonly getCondition: () => 'unfocused' | 'always' | undefined\n private readonly getIdleThresholdMs: () => number | undefined\n private readonly dialogThresholdMs: number\n private readonly state: PresenceState\n\n constructor(options: PresenceGateOptions) {\n this.getCondition = options.getCondition\n this.getIdleThresholdMs = options.getIdleThresholdMs\n this.dialogThresholdMs = options.dialogThresholdMs ?? DEFAULT_DIALOG_THRESHOLD_MS\n this.state = options.state\n }\n\n shouldNotify(kind: NotificationKind): boolean {\n if ((this.getCondition() ?? 'unfocused') === 'always') return true\n\n const focused = this.state.isFocused?.()\n if (focused !== undefined) return !focused\n\n const threshold =\n kind === 'turn_complete'\n ? (this.getIdleThresholdMs() ?? DEFAULT_IDLE_THRESHOLD_MS)\n : this.dialogThresholdMs\n return this.state.idleMs() >= threshold\n }\n}\n\n/** 默认门控:永远交付。脊柱无 PresenceGate 时的兜底(如纯单测)。 */\nexport const ALWAYS_PRESENT_GATE: PresenceGate = {\n shouldNotify: () => true,\n}\n","import type { PresenceState } from './gating'\n\nexport interface PresenceTrackerOptions {\n /** 时钟(可注入便于测试)。默认 Date.now。 */\n now?: () => number\n /** 是否启用焦点跟踪(DEC1004 opt-in)。关闭时 isFocused 恒返回 undefined → 时间戳模型。 */\n focusTracking?: boolean\n}\n\n/**\n * 在场状态的具体实现(RFC-047 N3)。脊柱(App)持有一份;交互/焦点信号由上游喂入:\n * - dispatcher 在 `prompt.start` 调 recordInteraction(默认、零 TUI 耦合)。\n * - TUI 可选在每次按键调 recordInteraction(更细)、DEC1004 焦点事件调 setFocus(opt-in)。\n */\nexport class PresenceTracker implements PresenceState {\n private lastInteractionAt: number\n private focus: boolean | undefined\n private readonly now: () => number\n\n constructor(options: PresenceTrackerOptions = {}) {\n this.now = options.now ?? (() => Date.now())\n this.lastInteractionAt = this.now()\n this.focus = options.focusTracking ? true : undefined\n }\n\n recordInteraction(): void {\n this.lastInteractionAt = this.now()\n }\n\n setFocus(focused: boolean): void {\n this.focus = focused\n }\n\n idleMs(): number {\n return this.now() - this.lastInteractionAt\n }\n\n isFocused(): boolean | undefined {\n return this.focus\n }\n}\n","import type { AgentSessionEvent } from '@x-otto/hook-contracts'\nimport { ALWAYS_PRESENT_GATE, type PresenceGate } from './gating'\nimport {\n NOTIFICATION_PRIORITY,\n type NotifiableSession,\n type NotificationChannel,\n type NotificationConfig,\n type NotificationKind,\n type NotificationPayload,\n} from './types'\n\nexport interface NotificationDispatcherOptions {\n /** 惰性读配置:settings 可 live-reload,故每次归一时取最新(RFC-047 §6.1 惰性门)。 */\n getConfig: () => NotificationConfig | undefined\n /** 在场门控(RFC-047 D5)。默认 always-present;N3 注入真实时间戳/焦点门。 */\n gate?: PresenceGate\n /** payload.cwd 解析。默认 process.cwd(AgentSessionEvent 不携 cwd)。 */\n getCwd?: () => string\n /**\n * 合并后的 flush 调度器。默认 queueMicrotask——同一同步 tick 内多事件先合并再交付一次\n * (busy turn 防刷屏,RFC-047 §2.1)。测试可注入捕获式调度器手动 flush。\n */\n schedule?: (flush: () => void) => void\n /** 通道交付出错回调。默认吞掉——通知绝不冒泡破坏会话(fire-and-forget)。 */\n onError?: (channelId: string, error: unknown) => void\n /** 用户活动回调(RFC-047 N3):dispatcher 在 prompt.start 调用,用于喂在场跟踪器。 */\n onInteraction?: () => void\n /** RFC-047 R4:有待跑 follow-up 则抑制完成通知(TUI 经 setFollowUpPendingProbe 注入读 state.queued)。 */\n isFollowUpPending?: () => boolean\n}\n\n/**\n * 通知脊柱(RFC-047 §6)。订阅 AgentSession EventBus 的触发事件 → 归一 →\n * 配置过滤 + 在场门控 → 单槽优先级合并 → 扇出到注册的交付通道。\n *\n * 前端无关:通过 NotifiableSession 结构型订阅、NotificationChannel 注册式交付,\n * 不依赖 runtime/tui。检测走 EventBus 单 substrate(RFC-047 D1);hook/终端/应用内\n * 通道由各前端注入注册(D2)。\n */\nexport class NotificationDispatcher {\n private readonly channels: NotificationChannel[] = []\n private readonly subscriptions = new Map<string, () => void>()\n private readonly getConfig: () => NotificationConfig | undefined\n private readonly gate: PresenceGate\n private readonly getCwd: () => string\n private readonly schedule: (flush: () => void) => void\n private readonly onError: (channelId: string, error: unknown) => void\n private readonly onInteraction: () => void\n private readonly isFollowUpPending: () => boolean\n\n private pending: NotificationPayload | null = null\n private flushScheduled = false\n /** 每会话累积的最后一段助手文本(喂 turn_complete 的 message/lastAssistantMessage)。 */\n private readonly lastAssistantText = new Map<string, string>()\n\n constructor(options: NotificationDispatcherOptions) {\n this.getConfig = options.getConfig\n this.gate = options.gate ?? ALWAYS_PRESENT_GATE\n this.getCwd = options.getCwd ?? (() => process.cwd())\n this.schedule = options.schedule ?? ((flush) => queueMicrotask(flush))\n this.onError = options.onError ?? (() => {})\n this.onInteraction = options.onInteraction ?? (() => {})\n this.isFollowUpPending = options.isFollowUpPending ?? (() => false)\n }\n\n /** 注册交付通道(D2:新增能力=注册,不改 dispatcher)。 */\n registerChannel(channel: NotificationChannel): void {\n this.channels.push(channel)\n }\n\n /** 订阅一个会话的 EventBus(在 app.createSession 构造层调用,全模式统一,D1)。 */\n attach(session: NotifiableSession): void {\n this.detach(session.id)\n const unsubscribe = session.subscribe((event) => this.handleEvent(event, session))\n this.subscriptions.set(session.id, unsubscribe)\n }\n\n /** 取消订阅(会话销毁时)。 */\n detach(sessionId: string): void {\n const unsubscribe = this.subscriptions.get(sessionId)\n if (unsubscribe) {\n unsubscribe()\n this.subscriptions.delete(sessionId)\n }\n this.lastAssistantText.delete(sessionId)\n }\n\n /** 释放全部订阅。 */\n dispose(): void {\n for (const unsubscribe of this.subscriptions.values()) unsubscribe()\n this.subscriptions.clear()\n this.lastAssistantText.clear()\n this.pending = null\n }\n\n /** 测试用:立即同步 flush 当前合并槽。 */\n flushNow(): void {\n this.flush()\n }\n\n private handleEvent(event: AgentSessionEvent, session: NotifiableSession): void {\n if (event.type === 'prompt.start') {\n this.lastAssistantText.delete(session.id)\n this.onInteraction()\n return\n }\n if (event.type === 'stream.event') {\n const inner = event.event\n if (inner.type === 'text_end' && inner.content.trim()) {\n this.lastAssistantText.set(session.id, inner.content)\n }\n return\n }\n const payload = this.normalize(event, session)\n if (payload) this.enqueue(payload)\n }\n\n private normalize(\n event: AgentSessionEvent,\n session: NotifiableSession,\n ): NotificationPayload | null {\n const config = this.getConfig() ?? {}\n if (config.enabled === false) return null\n\n const kind = mapKind(event)\n if (!kind) return null\n if (!passesConfigFilter(kind, config)) return null\n if (!this.gate.shouldNotify(kind)) return null\n if (kind === 'turn_complete' && this.isFollowUpPending()) return null\n\n const lastText = kind === 'turn_complete' ? this.lastAssistantText.get(session.id) : undefined\n if (kind === 'turn_complete') this.lastAssistantText.delete(session.id)\n const { title, message } = describe(kind, event, lastText)\n return {\n type: kind,\n title,\n message,\n sessionId: session.id,\n cwd: this.getCwd(),\n ...(lastText ? { lastAssistantMessage: lastText } : {}),\n }\n }\n\n /** 单槽合并:同 tick 内只保留最高(同优先级取最新)优先级那条,再调度一次 flush。 */\n private enqueue(payload: NotificationPayload): void {\n if (\n this.pending === null ||\n NOTIFICATION_PRIORITY[payload.type] >= NOTIFICATION_PRIORITY[this.pending.type]\n ) {\n this.pending = payload\n }\n if (!this.flushScheduled) {\n this.flushScheduled = true\n try {\n this.schedule(() => this.flush())\n } catch (error) {\n this.flushScheduled = false\n throw error\n }\n }\n }\n\n private flush(): void {\n this.flushScheduled = false\n const payload = this.pending\n this.pending = null\n if (!payload) return\n\n for (const channel of this.channels) {\n if (!channel.isAvailable()) continue\n try {\n const result = channel.deliver(payload)\n if (result instanceof Promise) {\n result.catch((error) => this.onError(channel.id, error))\n }\n } catch (error) {\n this.onError(channel.id, error)\n }\n }\n }\n}\n\n/** AgentSession 事件 → 归一类别(RFC-047 §6.2)。非触发事件返回 null。 */\nfunction mapKind(event: AgentSessionEvent): NotificationKind | null {\n switch (event.type) {\n case 'prompt.end':\n return 'turn_complete'\n case 'error':\n return 'error'\n case 'approval.required':\n case 'plan.approval.required':\n return 'approval_required'\n case 'ask.user.required':\n return 'input_required'\n default:\n return null\n }\n}\n\n/** on_* 配置开关过滤(OQ1:on_idle 是 on_completion 的 deprecated 别名,OR 合并)。 */\nfunction passesConfigFilter(kind: NotificationKind, config: NotificationConfig): boolean {\n switch (kind) {\n case 'turn_complete':\n return config.on_completion !== false || config.on_idle === true\n case 'error':\n return config.on_error !== false\n case 'approval_required':\n case 'input_required':\n return true\n }\n}\n\n/** 单行截断(终端/桌面通知不宜多行长文)。 */\nfunction snippet(text: string, max = 120): string {\n const oneLine = text.replace(/\\s+/g, ' ').trim()\n return oneLine.length > max ? `${oneLine.slice(0, max - 1)}…` : oneLine\n}\n\n/** 生成展示用 title/message。带外 payload 也复用 message。turn_complete 优先用最后助手文本。 */\nfunction describe(\n kind: NotificationKind,\n event: AgentSessionEvent,\n lastAssistantText?: string,\n): { title: string; message: string } {\n switch (kind) {\n case 'turn_complete':\n return {\n title: 'otto',\n message: lastAssistantText\n ? snippet(lastAssistantText)\n : 'Turn complete — otto finished responding',\n }\n case 'error':\n return {\n title: 'otto error',\n message:\n event.type === 'error'\n ? event.error instanceof Error\n ? event.error.message\n : String(event.error ?? 'Session error')\n : 'Session error',\n }\n case 'approval_required':\n if (event.type === 'approval.required') {\n return { title: 'Approval required', message: `${event.toolName} needs your approval` }\n }\n return { title: 'Approval required', message: 'A plan needs your approval' }\n case 'input_required':\n return {\n title: 'Input required',\n message:\n event.type === 'ask.user.required' ? event.question.question : 'otto needs your input',\n }\n }\n}\n","import type { NotificationChannel, NotificationPayload } from '../types'\n\n/**\n * 内存收集通道:把交付的 payload 累积到数组。用作脊柱自测的假通道(RFC-047 N1-06),\n * 也是 NotificationChannel 接口的最小参考实现。生产交付通道(终端/带外/应用内)见 N2/N4。\n */\nexport class MemoryChannel implements NotificationChannel {\n readonly id: string\n readonly delivered: NotificationPayload[] = []\n private available: boolean\n\n constructor(id = 'memory', available = true) {\n this.id = id\n this.available = available\n }\n\n isAvailable(): boolean {\n return this.available\n }\n\n setAvailable(available: boolean): void {\n this.available = available\n }\n\n deliver(payload: NotificationPayload): void {\n this.delivered.push(payload)\n }\n}\n","/** OSC terminal notifications: OSC 9 (progress) / OSC 777 (alert) sequence builders. */\n\nconst BEL = '\\x07'\nconst ESC = '\\x1b'\nconst ST = `${ESC}\\\\`\n\nexport type Multiplexer = 'none' | 'tmux' | 'screen'\n\n/**\n * 剥离 C0/C1 控制字节(码点 < 0x20,或 0x7f–0x9f,含 BEL/ESC/DEL)——**防转义注入**(review CRITICAL)。\n * payload.message/title 来自助手文本/错误/问题(模型或工具可影响),裸嵌入 OSC 时其中的\n * BEL 会提前终止序列、`\\x1b]…` 会注入新序列(改标题/写剪贴板/伪造超链接)。snippet 只折叠 \\s\n * 不含控制字节,且 error/input 路径绕过 snippet,故必须在构造器统一净化。用码点比较避免源码含控制字节。\n */\nfunction sanitize(s: string): string {\n let out = ''\n for (const ch of s) {\n const c = ch.codePointAt(0) ?? 0\n out += c < 0x20 || (c >= 0x7f && c <= 0x9f) ? ' ' : ch\n }\n return out\n}\n\n/** 终端响铃。裸 BEL——tmux 须看见原始 BEL 才置 window 活动标志,故**永不**包 DCS(规则4)。 */\nexport function bell(): string {\n return BEL\n}\n\n/** iTerm2 OSC 9 通知。 */\nexport function osc9(message: string): string {\n return `${ESC}]9;${sanitize(message)}${BEL}`\n}\n\n/** Kitty OSC 99 通知(title + body 两段;i=会话内去重 id,d=0 不替换,p 段类型)。 */\nexport function osc99(title: string, message: string, id = '1'): string {\n const t = `${ESC}]99;i=${id}:d=0:p=title;${sanitize(title)}${ST}`\n const b = `${ESC}]99;i=${id}:d=1:p=body;${sanitize(message)}${ST}`\n return t + b\n}\n\n/** Ghostty OSC 777 通知。 */\nexport function osc777(title: string, message: string): string {\n return `${ESC}]777;notify;${sanitize(title)};${sanitize(message)}${BEL}`\n}\n\n/**\n * multiplexer DCS 透传包装(仅 OSC 9/99/777,**不含 BEL**)。\n * tmux:`\\x1bPtmux;<内部 ESC 加倍>\\x1b\\\\`;screen:`\\x1bP<seq>\\x1b\\\\`。\n * 需 tmux `set -g allow-passthrough on`。\n */\nexport function wrapForMultiplexer(seq: string, mux: Multiplexer): string {\n if (mux === 'tmux') {\n return `${ESC}Ptmux;${seq.replace(/\\x1b/g, '\\x1b\\x1b')}${ST}`\n }\n if (mux === 'screen') {\n return `${ESC}P${seq}${ST}`\n }\n return seq\n}\n","import type { NotificationChannelName } from '../types'\nimport type { Multiplexer } from './osc'\n\n/** `auto` 能解析到的具体通道(排除 auto/disabled)。 */\nexport type ResolvedChannel = Exclude<NotificationChannelName, 'auto' | 'disabled'>\n\nexport interface TerminalCapabilities {\n /** 终端身份(调试用)。 */\n program: string\n multiplexer: Multiplexer\n /** `auto` 选择器解析出的通道(按终端能力)。 */\n autoChannel: ResolvedChannel\n}\n\ntype Env = Record<string, string | undefined>\n\n/**\n * 终端能力探测(RFC-047 N2-02)。`auto` 通道据此选具体 escape:\n * iTerm2 → OSC 9 / kitty → OSC 99 / ghostty → OSC 777 /\n * WezTerm·Warp → OSC 9(实证支持)/ 其余(含 Apple_Terminal)→ 响铃兜底。\n * 未知终端兜底 terminal_bell——BEL 通用安全,胜过盲发可能乱码的 OSC 或彻底无声。\n */\nexport function detectTerminal(env: Env = process.env): TerminalCapabilities {\n const termProgram = (env['TERM_PROGRAM'] ?? '').toLowerCase()\n const term = (env['TERM'] ?? '').toLowerCase()\n\n let autoChannel: ResolvedChannel = 'terminal_bell'\n if (termProgram === 'iterm.app' || termProgram === 'wezterm' || termProgram === 'warpterminal') {\n autoChannel = 'iterm2'\n } else if (term.includes('kitty') || env['KITTY_WINDOW_ID']) {\n autoChannel = 'kitty'\n } else if (termProgram === 'ghostty' || env['GHOSTTY_RESOURCES_DIR']) {\n autoChannel = 'ghostty'\n }\n\n return { program: termProgram || term || 'unknown', multiplexer: detectMultiplexer(env), autoChannel }\n}\n\n/** multiplexer 探测(决定 OSC 9/99/777 是否 DCS 透传;与 tui sync-output 的 tmux 判定关注点不同)。 */\nexport function detectMultiplexer(env: Env = process.env): Multiplexer {\n if (env['TMUX']) return 'tmux'\n if (env['STY'] || (env['TERM'] ?? '').startsWith('screen')) return 'screen'\n return 'none'\n}\n","import type { NotificationChannel, NotificationConfig, NotificationPayload } from '../types'\nimport { bell, osc9, osc99, osc777, wrapForMultiplexer, type Multiplexer } from '../terminal/osc'\nimport { detectTerminal, type ResolvedChannel } from '../terminal/detect'\n\nexport interface TerminalChannelOptions {\n /** 惰性读通知配置(channel 选择 + sound)。 */\n getConfig: () => NotificationConfig | undefined\n /** escape 写出。默认裸 process.stdout.write(不可见控制字节,安全,不走 Ink,N1 研究实证)。 */\n write?: (data: string) => void\n /** 是否 TTY(非 TTY=管道/headless 输出,不发 escape 避免污染)。 */\n isTty?: () => boolean\n /** 终端能力探测的 env 源。 */\n env?: Record<string, string | undefined>\n /** 测试静音(规则8)。默认 NODE_ENV==='test'。 */\n isTest?: () => boolean\n}\n\n/**\n * 带内终端通道(RFC-047 N2):把通知写成终端 escape(响铃 / iTerm2·Kitty·Ghostty 桌面通知)。\n * 纯 stdout escape,无 Ink/React 依赖 → 住在 @x-otto/notification,可被 CLI/TUI/headless 统一复用\n * (App 默认注册;非 TTY 自动失活,故管道/headless 输出不被污染)。\n */\nexport class TerminalChannel implements NotificationChannel {\n readonly id = 'terminal'\n private readonly getConfig: () => NotificationConfig | undefined\n private readonly write: (data: string) => void\n private readonly isTty: () => boolean\n private readonly env: Record<string, string | undefined>\n private readonly isTest: () => boolean\n\n constructor(options: TerminalChannelOptions) {\n this.getConfig = options.getConfig\n this.write = options.write ?? ((data) => void process.stdout.write(data))\n this.isTty = options.isTty ?? (() => Boolean(process.stdout.isTTY))\n this.env = options.env ?? process.env\n this.isTest = options.isTest ?? (() => process.env['NODE_ENV'] === 'test')\n }\n\n isAvailable(): boolean {\n if (this.isTest()) return false\n if (!this.isTty()) return false\n return (this.getConfig()?.channel ?? 'auto') !== 'disabled'\n }\n\n deliver(payload: NotificationPayload): void {\n const config = this.getConfig() ?? {}\n const requested = config.channel ?? 'auto'\n if (requested === 'disabled') return\n\n const caps = detectTerminal(this.env)\n const resolved: ResolvedChannel = requested === 'auto' ? caps.autoChannel : requested\n const sequence = buildSequence(resolved, payload, caps.multiplexer, Boolean(config.sound))\n if (sequence) this.write(sequence)\n }\n}\n\n/** 把归一 payload 渲染成所选通道的 escape(含 OQ3 的 sound→BEL 叠加)。 */\nfunction buildSequence(\n resolved: ResolvedChannel,\n payload: NotificationPayload,\n mux: Multiplexer,\n sound: boolean,\n): string {\n const title = payload.title ?? 'otto'\n\n if (resolved === 'terminal_bell') return bell()\n\n let seq: string\n switch (resolved) {\n case 'iterm2':\n case 'iterm2_with_bell':\n seq = osc9(`${title}: ${payload.message}`)\n break\n case 'kitty':\n seq = osc99(title, payload.message)\n break\n case 'ghostty':\n seq = osc777(title, payload.message)\n break\n }\n\n let out = wrapForMultiplexer(seq, mux)\n if (sound || resolved === 'iterm2_with_bell') out += bell()\n return out\n}\n","import type { NotificationChannel, NotificationPayload } from '../types'\n\n/**\n * 通用回调通道(RFC-047 N4):deliver 时调注入的回调。用于把脊柱接到外部机制而不引入依赖——\n * 如 HookChannel(回调 = `hookRegistry.emit('notification', ...)`,由 coding 注入)、\n * 应用内通道(回调 = `handle.pushNotification(...)`,由 CLI/TUI 注入)。\n */\nexport class CallbackChannel implements NotificationChannel {\n constructor(\n readonly id: string,\n private readonly available: () => boolean,\n private readonly callback: (payload: NotificationPayload) => void,\n ) {}\n\n isAvailable(): boolean {\n return this.available()\n }\n\n deliver(payload: NotificationPayload): void {\n this.callback(payload)\n }\n}\n","import { spawn as nodeSpawn } from 'node:child_process'\nimport type { NotificationChannel, NotificationConfig, NotificationPayload } from '../types'\n\ninterface WritableLike {\n write(data: string): void\n end(): void\n on?(event: 'error', listener: () => void): void\n}\ninterface ChildLike {\n stdin: WritableLike | null\n on?(event: 'error', listener: () => void): void\n unref?(): void\n}\nexport type SpawnLike = (command: string, args: string[]) => ChildLike\n\nexport interface CommandChannelOptions {\n getConfig: () => NotificationConfig | undefined\n /** 信任门(D6/规则2):项目级配置可来自克隆仓 → 未受信不 spawn,杜绝 RCE。 */\n isTrusted: () => boolean\n /** 可注入 spawn(测试)。默认 node child_process,argv 不过 shell(无插值=无 RCE 面)。 */\n spawn?: SpawnLike\n /** 测试静音(规则8)。默认 NODE_ENV==='test'。 */\n isTest?: () => boolean\n}\n\n/**\n * 带外外部程序通道(RFC-047 N4,`notify=[argv]` 模型):把通知 payload 作为 JSON 写入\n * 用户配置命令的 stdin(尾换行,OQ2),fire-and-forget,stdio 隔离,错误吞掉——绝不阻塞会话。\n * argv 直接 spawn 不过 shell(无引号/插值/RCE 面)。受信门控(项目可来自克隆仓)。\n */\nexport class CommandChannel implements NotificationChannel {\n readonly id = 'command'\n private readonly getConfig: () => NotificationConfig | undefined\n private readonly isTrusted: () => boolean\n private readonly spawn: SpawnLike\n private readonly isTest: () => boolean\n\n constructor(options: CommandChannelOptions) {\n this.getConfig = options.getConfig\n this.isTrusted = options.isTrusted\n this.spawn = options.spawn ?? defaultSpawn\n this.isTest = options.isTest ?? (() => process.env['NODE_ENV'] === 'test')\n }\n\n isAvailable(): boolean {\n if (this.isTest()) return false\n const command = this.getConfig()?.command\n return Boolean(command && command.length > 0) && this.isTrusted()\n }\n\n deliver(payload: NotificationPayload): void {\n const command = this.getConfig()?.command\n if (!command || command.length === 0) return\n if (this.isTest() || !this.isTrusted()) return\n\n const [program, ...args] = command\n if (program === undefined) return\n try {\n const child = this.spawn(program, args)\n child.on?.('error', () => {})\n child.stdin?.on?.('error', () => {})\n child.stdin?.write(`${JSON.stringify(payload)}\\n`)\n child.stdin?.end()\n child.unref?.()\n } catch {}\n }\n}\n\nconst defaultSpawn: SpawnLike = (command, args) =>\n nodeSpawn(command, args, { stdio: ['pipe', 'ignore', 'ignore'] }) as unknown as ChildLike\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@x-otto/notification",
3
+ "version": "0.0.1-alpha.0",
4
+ "files": [
5
+ "dist"
6
+ ],
7
+ "type": "module",
8
+ "main": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js"
14
+ }
15
+ },
16
+ "publishConfig": {
17
+ "access": "public",
18
+ "registry": "https://registry.npmjs.org",
19
+ "tag": "alpha"
20
+ },
21
+ "dependencies": {
22
+ "@x-otto/hook-contracts": "0.0.1-alpha.0",
23
+ "@x-otto/setting": "0.0.1-alpha.0"
24
+ },
25
+ "private": false,
26
+ "scripts": {
27
+ "build": "tsdown",
28
+ "typecheck:project": "tsc -p tsconfig.json --noEmit",
29
+ "typecheck": "tsc --noEmit",
30
+ "clean": "rm -rf dist"
31
+ }
32
+ }