cli-jaw 2.2.8 → 2.2.10

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.
Files changed (220) hide show
  1. package/README.md +4 -4
  2. package/dist/bin/agent-driven.js +33 -0
  3. package/dist/bin/agent-driven.js.map +1 -0
  4. package/dist/bin/commands/doctor.js +66 -1
  5. package/dist/bin/commands/doctor.js.map +1 -1
  6. package/dist/bin/commands/init.js +107 -8
  7. package/dist/bin/commands/init.js.map +1 -1
  8. package/dist/bin/interactive-confirm.js +110 -0
  9. package/dist/bin/interactive-confirm.js.map +1 -0
  10. package/dist/bin/star-prompt.js +46 -15
  11. package/dist/bin/star-prompt.js.map +1 -1
  12. package/dist/lib/upload.js +5 -1
  13. package/dist/lib/upload.js.map +1 -1
  14. package/dist/server.js +1 -0
  15. package/dist/server.js.map +1 -1
  16. package/dist/src/agent/alert-escalation.js +1 -1
  17. package/dist/src/agent/alert-escalation.js.map +1 -1
  18. package/dist/src/agent/codex-app-catalog.js +57 -0
  19. package/dist/src/agent/codex-app-catalog.js.map +1 -0
  20. package/dist/src/agent/codex-app-client.js +89 -7
  21. package/dist/src/agent/codex-app-client.js.map +1 -1
  22. package/dist/src/agent/pi-rpc-verdict.js +22 -0
  23. package/dist/src/agent/pi-rpc-verdict.js.map +1 -0
  24. package/dist/src/agent/pi-runtime.js +240 -0
  25. package/dist/src/agent/pi-runtime.js.map +1 -1
  26. package/dist/src/agent/runtime-pool.js +445 -0
  27. package/dist/src/agent/runtime-pool.js.map +1 -0
  28. package/dist/src/agent/spawn.js +395 -240
  29. package/dist/src/agent/spawn.js.map +1 -1
  30. package/dist/src/cli/command-context.js +5 -2
  31. package/dist/src/cli/command-context.js.map +1 -1
  32. package/dist/src/cli/commands.js +43 -39
  33. package/dist/src/cli/commands.js.map +1 -1
  34. package/dist/src/cli/handlers-runtime.js +15 -7
  35. package/dist/src/cli/handlers-runtime.js.map +1 -1
  36. package/dist/src/cli/handlers-search.js +1 -1
  37. package/dist/src/cli/handlers-search.js.map +1 -1
  38. package/dist/src/cli/handlers-skill-invoke.js +1 -1
  39. package/dist/src/cli/handlers-skill-invoke.js.map +1 -1
  40. package/dist/src/cli/handlers-workflows.js +1 -1
  41. package/dist/src/cli/handlers-workflows.js.map +1 -1
  42. package/dist/src/cli/handlers.js +1 -1
  43. package/dist/src/cli/handlers.js.map +1 -1
  44. package/dist/src/cli/opencodex-models.js +40 -7
  45. package/dist/src/cli/opencodex-models.js.map +1 -1
  46. package/dist/src/cli/registry-live.js +6 -5
  47. package/dist/src/cli/registry-live.js.map +1 -1
  48. package/dist/src/command-contract/catalog.js +4 -0
  49. package/dist/src/command-contract/catalog.js.map +1 -1
  50. package/dist/src/core/config.js +69 -4
  51. package/dist/src/core/config.js.map +1 -1
  52. package/dist/src/core/runtime-settings.js +17 -0
  53. package/dist/src/core/runtime-settings.js.map +1 -1
  54. package/dist/src/core/settings-merge.js +1 -1
  55. package/dist/src/core/settings-merge.js.map +1 -1
  56. package/dist/src/discord/bot.js +29 -14
  57. package/dist/src/discord/bot.js.map +1 -1
  58. package/dist/src/discord/commands.js +5 -4
  59. package/dist/src/discord/commands.js.map +1 -1
  60. package/dist/src/discord/discord-file.js +8 -2
  61. package/dist/src/discord/discord-file.js.map +1 -1
  62. package/dist/src/discord/forwarder.js +17 -19
  63. package/dist/src/discord/forwarder.js.map +1 -1
  64. package/dist/src/discord/send-only-client.js +9 -1
  65. package/dist/src/discord/send-only-client.js.map +1 -1
  66. package/dist/src/manager/dashboard-service.js +38 -1
  67. package/dist/src/manager/dashboard-service.js.map +1 -1
  68. package/dist/src/manager/telegram-hub/hub-bot.js +47 -29
  69. package/dist/src/manager/telegram-hub/hub-bot.js.map +1 -1
  70. package/dist/src/messaging/channel-health.js +32 -0
  71. package/dist/src/messaging/channel-health.js.map +1 -1
  72. package/dist/src/messaging/chunk.js +376 -0
  73. package/dist/src/messaging/chunk.js.map +1 -0
  74. package/dist/src/messaging/dedupe.js +104 -0
  75. package/dist/src/messaging/dedupe.js.map +1 -0
  76. package/dist/src/messaging/fold.js +210 -0
  77. package/dist/src/messaging/fold.js.map +1 -0
  78. package/dist/src/messaging/redact.js +577 -0
  79. package/dist/src/messaging/redact.js.map +1 -0
  80. package/dist/src/messaging/retry.js +76 -0
  81. package/dist/src/messaging/retry.js.map +1 -0
  82. package/dist/src/messaging/runtime.js +6 -3
  83. package/dist/src/messaging/runtime.js.map +1 -1
  84. package/dist/src/messaging/send.js +45 -15
  85. package/dist/src/messaging/send.js.map +1 -1
  86. package/dist/src/messaging/slack-target.js +50 -0
  87. package/dist/src/messaging/slack-target.js.map +1 -0
  88. package/dist/src/messaging/types.js.map +1 -1
  89. package/dist/src/orchestrator/pipeline.js +4 -4
  90. package/dist/src/orchestrator/pipeline.js.map +1 -1
  91. package/dist/src/routes/command.js +11 -7
  92. package/dist/src/routes/command.js.map +1 -1
  93. package/dist/src/routes/messaging.js +28 -12
  94. package/dist/src/routes/messaging.js.map +1 -1
  95. package/dist/src/routes/settings.js +22 -6
  96. package/dist/src/routes/settings.js.map +1 -1
  97. package/dist/src/slack/api.js +116 -0
  98. package/dist/src/slack/api.js.map +1 -0
  99. package/dist/src/slack/bot.js +262 -0
  100. package/dist/src/slack/bot.js.map +1 -0
  101. package/dist/src/slack/commands.js +98 -0
  102. package/dist/src/slack/commands.js.map +1 -0
  103. package/dist/src/slack/events.js +116 -0
  104. package/dist/src/slack/events.js.map +1 -0
  105. package/dist/src/slack/format.js +58 -0
  106. package/dist/src/slack/format.js.map +1 -0
  107. package/dist/src/slack/forwarder.js +57 -0
  108. package/dist/src/slack/forwarder.js.map +1 -0
  109. package/dist/src/slack/register.js +13 -0
  110. package/dist/src/slack/register.js.map +1 -0
  111. package/dist/src/slack/send-handler.js +41 -0
  112. package/dist/src/slack/send-handler.js.map +1 -0
  113. package/dist/src/slack/send-only-client.js +46 -0
  114. package/dist/src/slack/send-only-client.js.map +1 -0
  115. package/dist/src/slack/slack-file.js +72 -0
  116. package/dist/src/slack/slack-file.js.map +1 -0
  117. package/dist/src/slack/socket.js +341 -0
  118. package/dist/src/slack/socket.js.map +1 -0
  119. package/dist/src/telegram/bot.js +117 -37
  120. package/dist/src/telegram/bot.js.map +1 -1
  121. package/dist/src/telegram/elicitation-buttons.js +7 -2
  122. package/dist/src/telegram/elicitation-buttons.js.map +1 -1
  123. package/dist/src/telegram/forwarder.js +15 -7
  124. package/dist/src/telegram/forwarder.js.map +1 -1
  125. package/dist/src/telegram/rich-message.js +209 -24
  126. package/dist/src/telegram/rich-message.js.map +1 -1
  127. package/dist/src/telegram/telegram-file.js +35 -4
  128. package/dist/src/telegram/telegram-file.js.map +1 -1
  129. package/dist/src/telegram/voice.js +4 -3
  130. package/dist/src/telegram/voice.js.map +1 -1
  131. package/package.json +5 -1
  132. package/public/assets/providers/slack.svg +1 -0
  133. package/public/dist/assets/ChannelsDiscord-D9FLnZso.js +1 -0
  134. package/public/dist/assets/ChannelsSlack-ybvupgdK.js +1 -0
  135. package/public/dist/assets/ChannelsTelegram-BOCOPrBc.js +1 -0
  136. package/public/dist/assets/HealthBadge-megTlF4n.js +1 -0
  137. package/public/dist/assets/TransportStatusChips-CjEySMis.js +1 -0
  138. package/public/dist/assets/app-BGIAy4he.js +55 -0
  139. package/public/dist/assets/{bgtask-badge-CdBqBy1L.js → bgtask-badge-DXca8E7c.js} +1 -1
  140. package/public/dist/assets/bgtask-badge-GRSiNILQ.js +1 -0
  141. package/public/dist/assets/{employees-C8qufeJl.js → employees-BL4qMxJj.js} +1 -1
  142. package/public/dist/assets/iframe-renderer-BUSk0Rw9.js +1 -0
  143. package/public/dist/assets/{iframe-renderer-CZ67aeTO.js → iframe-renderer-DUI0LYBy.js} +2 -2
  144. package/public/dist/assets/{manager-CLXT60M5.js → manager-B-gpf-3j.js} +3 -3
  145. package/public/dist/assets/{memory-lWramLVw.js → memory-B_Y0L0KD.js} +1 -1
  146. package/public/dist/assets/memory-CX6YahTc.js +1 -0
  147. package/public/dist/assets/message-history-Det7u76S.js +1 -0
  148. package/public/dist/assets/{message-history-2NUN3UxM.js → message-history-Dl8dJzez.js} +1 -1
  149. package/public/dist/assets/{provider-icons-stsNmQKx.js → provider-icons-VDfahiUp.js} +3 -2
  150. package/public/dist/assets/{render-CsF-ojLe.js → render-BI1aqKoc.js} +1 -1
  151. package/public/dist/assets/{settings-BMLowRCh.js → settings-B0Sj4sUH.js} +1 -1
  152. package/public/dist/assets/settings-C7NutP6X.js +1 -0
  153. package/public/dist/assets/settings-core-CyX-px6_.js +100 -0
  154. package/public/dist/assets/settings-core-DVpFmrhY.js +1 -0
  155. package/public/dist/assets/{sidebar-gKjYLv49.js → sidebar-DOR9fTgN.js} +3 -3
  156. package/public/dist/assets/skills-BWhKreQ2.js +1 -0
  157. package/public/dist/assets/{skills-CKTbomdm.js → skills-wt0fkV9Y.js} +1 -1
  158. package/public/dist/assets/slash-commands-BZbqTg3v.js +20 -0
  159. package/public/dist/assets/slash-commands-Di4NDA4w.js +1 -0
  160. package/public/dist/assets/{trace-drawer-C3G4mmB1.js → trace-drawer-kMYzaVFW.js} +1 -1
  161. package/public/dist/assets/ui-BghexWrB.js +1 -0
  162. package/public/dist/assets/{ui-CVVC_flX.js → ui-D4D5HUfo.js} +1 -1
  163. package/public/dist/index.html +62 -1
  164. package/public/dist/manager/index.html +1 -1
  165. package/public/index.html +61 -0
  166. package/public/js/features/command-info.ts +1 -0
  167. package/public/js/features/help-content.ts +8 -0
  168. package/public/js/features/settings-channel.ts +5 -1
  169. package/public/js/features/settings-core.ts +2 -0
  170. package/public/js/features/settings-slack.ts +74 -0
  171. package/public/js/features/settings-types.ts +5 -1
  172. package/public/js/features/settings.ts +1 -0
  173. package/public/js/features/transport-status-row.ts +15 -3
  174. package/public/js/main.ts +19 -0
  175. package/public/js/provider-icons.ts +4 -1
  176. package/public/js/ws.ts +1 -1
  177. package/public/locales/en.json +21 -0
  178. package/public/locales/ja.json +21 -0
  179. package/public/locales/ko.json +21 -0
  180. package/public/locales/zh.json +21 -0
  181. package/public/manager/src/settings/SettingsShell.tsx +1 -0
  182. package/public/manager/src/settings/SettingsSidebar.tsx +1 -0
  183. package/public/manager/src/settings/pages/ChannelsSlack.tsx +323 -0
  184. package/public/manager/src/settings/pages/components/ActiveChannelToggle.tsx +2 -1
  185. package/public/manager/src/settings/pages/components/TransportStatusChips.tsx +13 -4
  186. package/public/manager/src/settings/types.ts +1 -0
  187. package/scripts/check-redaction-sinks.mjs +276 -0
  188. package/scripts/pi-rpc-probe.mts +361 -0
  189. package/scripts/release-gates.mjs +83 -0
  190. package/scripts/release-preview.sh +8 -1
  191. package/scripts/release.sh +13 -2
  192. package/scripts/sync-electron-version.cjs +93 -0
  193. package/public/dist/assets/ChannelsDiscord-Bt1_rtNC.js +0 -1
  194. package/public/dist/assets/ChannelsTelegram-BLV0m1oq.js +0 -1
  195. package/public/dist/assets/TransportStatusChips-Bld0yXi7.js +0 -1
  196. package/public/dist/assets/app-iSJhsP20.js +0 -55
  197. package/public/dist/assets/bgtask-badge-CSkSsoB6.js +0 -1
  198. package/public/dist/assets/iframe-renderer-DXdo0YXN.js +0 -1
  199. package/public/dist/assets/memory-DeKUlD2M.js +0 -1
  200. package/public/dist/assets/message-history-CWTQ2woA.js +0 -1
  201. package/public/dist/assets/settings-CDjs7hTT.js +0 -1
  202. package/public/dist/assets/settings-core-Cs_oq-b3.js +0 -99
  203. package/public/dist/assets/settings-core-D-TOh1a6.js +0 -1
  204. package/public/dist/assets/skills-GuIfNASs.js +0 -1
  205. package/public/dist/assets/slash-commands-BtZa093n.js +0 -20
  206. package/public/dist/assets/slash-commands-DGJKfpMT.js +0 -1
  207. package/public/dist/assets/ui-DJg5Q_NJ.js +0 -1
  208. /package/public/dist/assets/{DashboardMeta-B7CVqXcU.js → DashboardMeta-DennaaA_.js} +0 -0
  209. /package/public/dist/assets/{Display-BF-rACK6.js → Display-hGuSn_W5.js} +0 -0
  210. /package/public/dist/assets/{Employees-D3hnciP0.js → Employees-B5YXk5ma.js} +0 -0
  211. /package/public/dist/assets/{Heartbeat-D4Pn5O3O.js → Heartbeat-vDGhPfct.js} +0 -0
  212. /package/public/dist/assets/{Mcp-SfytKWGX.js → Mcp-BHWnJ6SP.js} +0 -0
  213. /package/public/dist/assets/{Memory-DNxBOQ7N.js → Memory-B_UZ-iCA.js} +0 -0
  214. /package/public/dist/assets/{ModelProvider-CXwtlG7l.js → ModelProvider-CgUvs6pE.js} +0 -0
  215. /package/public/dist/assets/{Network-DISUhige.js → Network-AWpSkALD.js} +0 -0
  216. /package/public/dist/assets/{Profile-B8yBO3Ju.js → Profile-DnIE2ios.js} +0 -0
  217. /package/public/dist/assets/{Prompts-5lNQ5qOm.js → Prompts-B9aKLall.js} +0 -0
  218. /package/public/dist/assets/{SpeechKeys-BepaIHBa.js → SpeechKeys-K4Be_o7z.js} +0 -0
  219. /package/public/dist/assets/{TelegramHub--DNjRAwW.js → TelegramHub-CFwSkzHP.js} +0 -0
  220. /package/public/dist/assets/{TerminalPanel-DQovYfwz.js → TerminalPanel-CdlFZHAK.js} +0 -0
@@ -0,0 +1,361 @@
1
+ #!/usr/bin/env -S npx tsx
2
+
3
+ import { spawn, spawnSync, type ChildProcessWithoutNullStreams } from 'node:child_process';
4
+ import fs from 'node:fs';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+ import { StringDecoder } from 'node:string_decoder';
8
+ import {
9
+ DEFAULT_PI_PROFILE,
10
+ DEFAULT_PI_SETTINGS,
11
+ ensurePiRuntimeConfig,
12
+ parsePiRpcRecord,
13
+ resolvePiCommand,
14
+ type PiCommand,
15
+ } from '../src/agent/pi-runtime.js';
16
+ import { JAW_HOME } from '../src/core/config.js';
17
+ import {
18
+ classifySecondPromptOutcome,
19
+ type PiRpcVerdict,
20
+ type PiRpcVerdictRecord,
21
+ } from '../src/agent/pi-rpc-verdict.js';
22
+
23
+ const GET_STATE_ID = 1;
24
+ const FIRST_PROMPT_ID = 2;
25
+ const SECOND_PROMPT_ID = 3;
26
+ const THIRD_PROMPT_ID = 4;
27
+ const ABORT_ID = 5;
28
+ const FIRST_TIMEOUT_MS = 60_000;
29
+ const SECOND_TIMEOUT_MS = 30_000;
30
+ const ABORT_START_TIMEOUT_MS = 30_000;
31
+ const ABORT_TERMINAL_TIMEOUT_MS = 15_000;
32
+
33
+ type RawRecord = Record<string, unknown>;
34
+ type ObservedRecord = { raw: RawRecord; normalized: PiRpcVerdictRecord };
35
+ type WaitResult = { matched: boolean; closed: boolean; timedOut: boolean };
36
+ type AttemptResult = {
37
+ verdict: PiRpcVerdict;
38
+ abortEffective: boolean;
39
+ firstPromptSucceeded: boolean;
40
+ selfExited: boolean;
41
+ secondTimedOut: boolean;
42
+ recordCount: number;
43
+ };
44
+
45
+ function normalizeRecord(raw: RawRecord): PiRpcVerdictRecord {
46
+ const parsed = parsePiRpcRecord(raw);
47
+ const normalized: PiRpcVerdictRecord = {};
48
+ if (typeof raw['id'] === 'number') normalized.id = raw['id'];
49
+ if (typeof raw['type'] === 'string') normalized.type = raw['type'];
50
+ if (raw['type'] === 'response') {
51
+ if (typeof raw['command'] === 'string') normalized.command = raw['command'];
52
+ if (typeof raw['success'] === 'boolean') normalized.success = raw['success'];
53
+ }
54
+ const error = raw['error'];
55
+ if (error && typeof error === 'object') {
56
+ const message = (error as Record<string, unknown>)['message'];
57
+ normalized.error = typeof message === 'string' ? { message } : {};
58
+ }
59
+ if (parsed.done !== undefined) normalized.done = parsed.done;
60
+ if (parsed.text !== undefined) normalized.text = parsed.text;
61
+ if (raw['type'] === 'agent_end') {
62
+ // user echo 추출 — 모델이 text 파트를 생략하는 비결정성에 강건한 턴 상관 증거
63
+ const messages = raw['messages'];
64
+ if (Array.isArray(messages)) {
65
+ normalized.userEcho = messages
66
+ .filter((entry) => !!entry && typeof entry === 'object' && (entry as Record<string, unknown>)['role'] === 'user')
67
+ .map((entry) => extractProbeText((entry as Record<string, unknown>)['content']))
68
+ .join('');
69
+ }
70
+ }
71
+ return normalized;
72
+ }
73
+
74
+ function extractProbeText(value: unknown): string {
75
+ if (typeof value === 'string') return value;
76
+ if (Array.isArray(value)) return value.map(extractProbeText).join('');
77
+ if (!value || typeof value !== 'object') return '';
78
+ const obj = value as Record<string, unknown>;
79
+ if (obj['type'] === 'thinking') return '';
80
+ return extractProbeText(obj['text']) || extractProbeText(obj['content']) || '';
81
+ }
82
+
83
+ function isSuccessfulStateResponse(record: ObservedRecord): boolean {
84
+ return record.normalized.id === GET_STATE_ID
85
+ && record.raw['type'] === 'response'
86
+ && record.raw['command'] === 'get_state'
87
+ && record.raw['success'] === true;
88
+ }
89
+
90
+ function isGenerationEvent(record: ObservedRecord): boolean {
91
+ const type = record.raw['type'];
92
+ if (type === 'agent_end') return false;
93
+ if (record.normalized.text) return true;
94
+ return typeof type === 'string' && type !== 'response';
95
+ }
96
+
97
+ function isAbortAccepted(record: ObservedRecord): boolean {
98
+ return record.normalized.id === ABORT_ID
99
+ && record.raw['type'] === 'response'
100
+ && record.raw['command'] === 'abort'
101
+ && record.raw['success'] === true;
102
+ }
103
+
104
+ function isTerminalRecord(record: ObservedRecord): boolean {
105
+ if (record.normalized.done) return true;
106
+ const data = record.raw['data'];
107
+ if (!data || typeof data !== 'object') return false;
108
+ const state = data as Record<string, unknown>;
109
+ if (state['running'] === false || state['isRunning'] === false) return true;
110
+ return typeof state['state'] === 'string' && !['running', 'active', 'generating'].includes(state['state'].toLowerCase());
111
+ }
112
+
113
+ class RpcProbeProcess {
114
+ readonly records: ObservedRecord[] = [];
115
+ readonly child: ChildProcessWithoutNullStreams;
116
+ private readonly listeners = new Set<() => void>();
117
+ private buffer = '';
118
+ private readonly decoder = new StringDecoder('utf8');
119
+ private closed = false;
120
+ private closeCode: number | null = null;
121
+
122
+ constructor(command: PiCommand, runtimeDir: string) {
123
+ const args = [
124
+ ...command.baseArgs,
125
+ '--mode', 'rpc',
126
+ '--no-context-files',
127
+ '--provider', DEFAULT_PI_PROFILE.id,
128
+ '--model', DEFAULT_PI_PROFILE.model,
129
+ '--api-key', DEFAULT_PI_PROFILE.apiKey || 'dummy',
130
+ ];
131
+ console.log(`[probe:spawn] ${JSON.stringify([command.command, ...args])}`);
132
+ this.child = spawn(command.command, args, {
133
+ cwd: process.cwd(),
134
+ env: { ...process.env, PI_CODING_AGENT_DIR: runtimeDir },
135
+ stdio: ['pipe', 'pipe', 'pipe'],
136
+ ...(process.platform === 'win32' && !command.command.toLowerCase().endsWith('.exe') ? { shell: true } : {}),
137
+ });
138
+ this.child.stdout.on('data', (chunk: Buffer) => this.consumeStdout(chunk));
139
+ this.child.stderr.on('data', (chunk: Buffer) => process.stderr.write(`[probe:stderr] ${chunk.toString()}`));
140
+ this.child.on('error', (error) => {
141
+ console.error(`[probe:error] ${error.message}`);
142
+ this.notify();
143
+ });
144
+ this.child.on('close', (code) => {
145
+ this.buffer += this.decoder.end();
146
+ if (this.buffer.trim()) this.consumeLine(this.buffer.trim());
147
+ this.buffer = '';
148
+ this.closed = true;
149
+ this.closeCode = code;
150
+ console.log(`[probe:close] code=${String(code)}`);
151
+ this.notify();
152
+ });
153
+ }
154
+
155
+ get didClose(): boolean { return this.closed; }
156
+ get exitedNormally(): boolean { return this.closed && this.closeCode === 0; }
157
+
158
+ send(id: number, type: string, fields: RawRecord = {}): Promise<void> {
159
+ const line = JSON.stringify({ id, type, ...fields });
160
+ console.log(`[probe:send] ${line}`);
161
+ return new Promise((resolve, reject) => {
162
+ if (!this.child.stdin.writable) {
163
+ reject(new Error('pi rpc stdin is not writable'));
164
+ return;
165
+ }
166
+ this.child.stdin.write(`${line}\n`, (error) => error ? reject(error) : resolve());
167
+ });
168
+ }
169
+
170
+ async waitFrom(start: number, predicate: (records: ObservedRecord[]) => boolean, timeoutMs: number): Promise<WaitResult> {
171
+ if (predicate(this.records.slice(start))) return { matched: true, closed: this.closed, timedOut: false };
172
+ if (this.closed) return { matched: false, closed: true, timedOut: false };
173
+ return new Promise((resolve) => {
174
+ const finish = (result: WaitResult) => {
175
+ clearTimeout(timer);
176
+ this.listeners.delete(check);
177
+ resolve(result);
178
+ };
179
+ const check = () => {
180
+ if (predicate(this.records.slice(start))) finish({ matched: true, closed: this.closed, timedOut: false });
181
+ else if (this.closed) finish({ matched: false, closed: true, timedOut: false });
182
+ };
183
+ const timer = setTimeout(() => finish({ matched: false, closed: this.closed, timedOut: true }), timeoutMs);
184
+ this.listeners.add(check);
185
+ });
186
+ }
187
+
188
+ async stop(): Promise<void> {
189
+ try { this.child.stdin.end(); } catch { /* already closed */ }
190
+ if (this.closed) return;
191
+ if (!this.child.killed) this.child.kill('SIGTERM');
192
+ const stopped = await this.waitFrom(this.records.length, () => false, 2_000);
193
+ if (!stopped.closed) {
194
+ this.child.kill('SIGKILL');
195
+ await this.waitFrom(this.records.length, () => false, 1_000);
196
+ }
197
+ }
198
+
199
+ private consumeStdout(chunk: Buffer): void {
200
+ this.buffer += this.decoder.write(chunk);
201
+ const lines = this.buffer.split('\n');
202
+ this.buffer = lines.pop() ?? '';
203
+ for (const line of lines) if (line.trim()) this.consumeLine(line.trim());
204
+ }
205
+
206
+ private consumeLine(line: string): void {
207
+ process.stdout.write(`${line}\n`);
208
+ try {
209
+ const raw = JSON.parse(line) as unknown;
210
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return;
211
+ this.records.push({ raw: raw as RawRecord, normalized: normalizeRecord(raw as RawRecord) });
212
+ this.notify();
213
+ } catch {
214
+ console.error(`[probe:parse-error] ${line.slice(0, 200)}`);
215
+ }
216
+ }
217
+
218
+ private notify(): void {
219
+ for (const listener of [...this.listeners]) listener();
220
+ }
221
+ }
222
+
223
+ async function probeAbort(rpc: RpcProbeProcess): Promise<boolean> {
224
+ if (rpc.didClose) return false;
225
+ const thirdStart = rpc.records.length;
226
+ try {
227
+ await rpc.send(THIRD_PROMPT_ID, 'prompt', { message: 'Write a very long story, at least 2000 words.' });
228
+ } catch (error) {
229
+ console.error(`[probe:abort] third prompt write failed: ${(error as Error).message}`);
230
+ return false;
231
+ }
232
+ const started = await rpc.waitFrom(thirdStart, (records) => records.some(isGenerationEvent), ABORT_START_TIMEOUT_MS);
233
+ if (!started.matched || rpc.didClose) return false;
234
+
235
+ const abortStart = rpc.records.length;
236
+ try {
237
+ await rpc.send(ABORT_ID, 'abort');
238
+ } catch (error) {
239
+ console.error(`[probe:abort] abort write failed: ${(error as Error).message}`);
240
+ return false;
241
+ }
242
+ const terminal = await rpc.waitFrom(abortStart, (records) => {
243
+ const accepted = records.some(isAbortAccepted);
244
+ return accepted && records.some(isTerminalRecord);
245
+ }, ABORT_TERMINAL_TIMEOUT_MS);
246
+ return terminal.matched;
247
+ }
248
+
249
+ async function runAttempt(command: PiCommand, attempt: number): Promise<AttemptResult> {
250
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), `cli-jaw-pi-rpc-probe-${attempt}-`));
251
+ const runtimeDir = ensurePiRuntimeConfig(DEFAULT_PI_SETTINGS, DEFAULT_PI_PROFILE.id, '', tempRoot);
252
+ const rpc = new RpcProbeProcess(command, runtimeDir);
253
+ let firstPromptSucceeded = false;
254
+ let selfExited = false;
255
+ let secondTimedOut = false;
256
+ let abortEffective = false;
257
+ let secondRecords: ObservedRecord[] = [];
258
+ try {
259
+ const stateStart = rpc.records.length;
260
+ await rpc.send(GET_STATE_ID, 'get_state');
261
+ const state = await rpc.waitFrom(stateStart, (records) => records.some(isSuccessfulStateResponse), 15_000);
262
+ if (!state.matched) {
263
+ return { verdict: 'inconclusive', abortEffective, firstPromptSucceeded, selfExited, secondTimedOut: state.timedOut, recordCount: rpc.records.length };
264
+ }
265
+
266
+ const firstStart = rpc.records.length;
267
+ await rpc.send(FIRST_PROMPT_ID, 'prompt', { message: 'Reply with the single token: FIRST' });
268
+ const first = await rpc.waitFrom(firstStart, (records) => records.some((record) => record.normalized.done === true), FIRST_TIMEOUT_MS);
269
+ firstPromptSucceeded = first.matched;
270
+
271
+ const secondStart = rpc.records.length;
272
+ if (firstPromptSucceeded) {
273
+ try {
274
+ await rpc.send(SECOND_PROMPT_ID, 'prompt', { message: 'Reply with the single token: SECOND' });
275
+ } catch (error) {
276
+ console.error(`[probe:second] write failed: ${(error as Error).message}`);
277
+ selfExited = rpc.exitedNormally;
278
+ }
279
+ if (!selfExited) {
280
+ const second = await rpc.waitFrom(secondStart, (records) => records.some((record) =>
281
+ record.normalized.done === true
282
+ || (record.normalized.id === SECOND_PROMPT_ID && record.normalized.error !== undefined)
283
+ ), SECOND_TIMEOUT_MS);
284
+ secondTimedOut = second.timedOut;
285
+ secondRecords = rpc.records.slice(secondStart);
286
+ if (second.closed && rpc.exitedNormally && !second.matched) selfExited = true;
287
+ if (!secondTimedOut && !rpc.didClose) abortEffective = await probeAbort(rpc);
288
+ }
289
+ }
290
+
291
+ const verdict = classifySecondPromptOutcome({
292
+ records: secondRecords.map((record) => record.normalized),
293
+ secondPromptId: SECOND_PROMPT_ID,
294
+ timedOut: secondTimedOut,
295
+ selfExited,
296
+ firstPromptSucceeded,
297
+ });
298
+ if (process.env['PI_RPC_PROBE_DEBUG']) {
299
+ console.error(`[probe:debug] classifier input: ${JSON.stringify({
300
+ firstPromptSucceeded, selfExited, secondTimedOut,
301
+ doneRecords: secondRecords.filter((r) => r.normalized.done).map((r) => r.normalized),
302
+ secondRecordCount: secondRecords.length,
303
+ })}`);
304
+ }
305
+ return { verdict, abortEffective, firstPromptSucceeded, selfExited, secondTimedOut, recordCount: rpc.records.length };
306
+ } catch (error) {
307
+ console.error(`[probe:attempt-${attempt}] ${(error as Error).stack || String(error)}`);
308
+ return { verdict: 'inconclusive', abortEffective, firstPromptSucceeded, selfExited, secondTimedOut, recordCount: rpc.records.length };
309
+ } finally {
310
+ await rpc.stop();
311
+ fs.rmSync(tempRoot, { recursive: true, force: true });
312
+ }
313
+ }
314
+
315
+ function resolveCommandIdentity(command: PiCommand): string {
316
+ const result = spawnSync(command.command, [...command.baseArgs, '--version'], {
317
+ encoding: 'utf8',
318
+ env: process.env,
319
+ timeout: 15_000,
320
+ });
321
+ const version = `${result.stdout || ''}\n${result.stderr || ''}`.trim() || `exit:${String(result.status)}`;
322
+ return JSON.stringify({ source: command.source, command: command.command, baseArgs: command.baseArgs, version });
323
+ }
324
+
325
+ function writeCapability(commandId: string, result: AttemptResult, attempts: AttemptResult[]): void {
326
+ const targetDir = path.join(JAW_HOME, 'pi');
327
+ const target = path.join(targetDir, 'rpc-capabilities.json');
328
+ const temp = `${target}.${process.pid}.tmp`;
329
+ const evidence = attempts.map((attempt, index) =>
330
+ `attempt=${index + 1},verdict=${attempt.verdict},first=${attempt.firstPromptSucceeded},selfExited=${attempt.selfExited},secondTimedOut=${attempt.secondTimedOut},abort=${attempt.abortEffective},records=${attempt.recordCount}`
331
+ ).join('; ');
332
+ fs.mkdirSync(targetDir, { recursive: true });
333
+ fs.writeFileSync(temp, `${JSON.stringify({
334
+ schemaVersion: 1,
335
+ commandId,
336
+ probedAt: new Date().toISOString(),
337
+ profileId: DEFAULT_PI_PROFILE.id,
338
+ abortEffective: result.abortEffective,
339
+ evidence,
340
+ }, null, 2)}\n`, 'utf8');
341
+ fs.renameSync(temp, target);
342
+ console.log(`[probe:capabilities] ${target}`);
343
+ }
344
+
345
+ async function main(): Promise<void> {
346
+ const command = resolvePiCommand();
347
+ const commandId = resolveCommandIdentity(command);
348
+ const attempts: AttemptResult[] = [];
349
+ attempts.push(await runAttempt(command, 1));
350
+ if (attempts[0]?.verdict === 'inconclusive') attempts.push(await runAttempt(command, 2));
351
+ const result = attempts.at(-1);
352
+ if (!result) throw new Error('pi rpc probe produced no result');
353
+ writeCapability(commandId, result, attempts);
354
+ console.log(`[probe:verdict] ${result.verdict} abortEffective=${result.abortEffective}`);
355
+ process.exitCode = result.verdict === 'supported' ? 0 : result.verdict === 'proven-unsupported' ? 1 : 2;
356
+ }
357
+
358
+ void main().catch((error: unknown) => {
359
+ console.error(`[probe:fatal] ${(error as Error).stack || String(error)}`);
360
+ process.exitCode = 2;
361
+ });
@@ -394,6 +394,89 @@ const GATES = {
394
394
  return { ok: true, detail: 'live any/debt/allow counts within frozen baseline' };
395
395
  },
396
396
  },
397
+ 'redaction-sinks': {
398
+ description: 'channel replies, sends and loggers route through a credential masker',
399
+ check() {
400
+ const r = run('node', ['scripts/check-redaction-sinks.mjs'], { timeout: 60_000 });
401
+ if (r.status !== 0) {
402
+ return { ok: false, detail: `unmasked channel sink:\n${(r.stdout || r.stderr || '').slice(-1500)}` };
403
+ }
404
+ return { ok: true, detail: 'no unmasked sink in telegram/discord/slack/telegram-hub' };
405
+ },
406
+ },
407
+ 'electron-version': {
408
+ description: 'electron/package.json version matches the root package.json',
409
+ check() {
410
+ const r = run('node', ['scripts/sync-electron-version.cjs', '--check'], { timeout: 60_000 });
411
+ if (r.status !== 0) {
412
+ return { ok: false, detail: (r.stdout || r.stderr || '').trim().slice(-500) };
413
+ }
414
+ return { ok: true, detail: 'desktop artifacts will carry the release version' };
415
+ },
416
+ },
417
+ 'gate-docs': {
418
+ description: 'structure/INDEX.md documents exactly the gates that exist, and each is npm-addressable',
419
+ check() {
420
+ // structure/INDEX.md hardcodes the gate count and enumerates every
421
+ // name. Adding the 16th gate made that row wrong, and nothing
422
+ // noticed: check-docs.mts counts only routes and endpoints, and
423
+ // check-doc-drift.sh never looks at gates. Fixing the row by hand
424
+ // cleared one instance; this closes the class.
425
+ //
426
+ // This gate lives inside release-gates.mjs rather than in its own
427
+ // script because the module calls main() unconditionally at the
428
+ // bottom, so importing GATES from outside would run every gate.
429
+ const names = Object.keys(GATES);
430
+ const doc = readFile('structure/INDEX.md');
431
+ const row = doc.split('\n').find((line) =>
432
+ line.includes('named gates') && line.includes('runs all'));
433
+ if (!row) {
434
+ return { ok: false, detail: 'the release-gates row is gone from structure/INDEX.md' };
435
+ }
436
+
437
+ const problems = [];
438
+
439
+ const claimed = row.match(/runs all (\d+) named gates/);
440
+ if (!claimed) {
441
+ problems.push('the row no longer states a gate count');
442
+ } else if (Number(claimed[1]) !== names.length) {
443
+ problems.push(`count says ${claimed[1]}, GATES has ${names.length}`);
444
+ }
445
+
446
+ // Read ONLY the parenthesised list, not the whole row. The row also
447
+ // carries `scripts/release-gates.mjs`, `package.json`, `gate:all`
448
+ // and `gate:<name>` in prose; scanning the row and filtering those
449
+ // out by shape both accuses innocent prose and lets a phantom like
450
+ // `gate:retired-name` through, because a filter keyed on the token
451
+ // shape cannot tell a stale gate from an ordinary mention.
452
+ const list = row.match(/named gates \(([^)]*)\)/);
453
+ if (!list) {
454
+ problems.push('the parenthesised gate list is gone from the row');
455
+ } else {
456
+ const documented = [...list[1].matchAll(/`([^`]+)`/g)].map((m) => m[1]);
457
+ const missing = names.filter((name) => !documented.includes(name));
458
+ const stale = documented.filter((name) => !names.includes(name));
459
+ if (missing.length > 0) problems.push(`undocumented: ${missing.join(', ')}`);
460
+ if (stale.length > 0) problems.push(`documented but gone: ${stale.join(', ')}`);
461
+ }
462
+
463
+ // The same row promises `gate:<name>` addressability. A gate added
464
+ // without its npm script makes that sentence false while gate:all
465
+ // stays green -- the hand-maintained invariant this gate exists to
466
+ // stop being hand-maintained.
467
+ const pkg = JSON.parse(readFile('package.json'));
468
+ const unaddressable = names.filter((name) =>
469
+ pkg.scripts?.[`gate:${name}`] !== `node scripts/release-gates.mjs ${name}`);
470
+ if (unaddressable.length > 0) {
471
+ problems.push(`no npm script: ${unaddressable.map((n) => `gate:${n}`).join(', ')}`);
472
+ }
473
+
474
+ if (problems.length > 0) {
475
+ return { ok: false, detail: `${problems.join('\n')}\nFix structure/INDEX.md / package.json` };
476
+ }
477
+ return { ok: true, detail: `${names.length} gates documented and addressable` };
478
+ },
479
+ },
397
480
  };
398
481
 
399
482
  function printResult(name, result) {
@@ -148,6 +148,13 @@ npm version "$PREVIEW_VERSION" --no-git-tag-version
148
148
  VERSION=$(node -p "require('./package.json').version")
149
149
  echo "📌 package.json version: $VERSION"
150
150
 
151
+ # Same reason as release.sh: the committed Electron version names the desktop
152
+ # artifacts. This also has to run BEFORE gate:all below -- the electron-version
153
+ # gate compares the two manifests, and the bump above has just moved the root
154
+ # one, so skipping this would fail the preview release outright.
155
+ echo "🖥️ Syncing Electron version to $VERSION..."
156
+ node scripts/sync-electron-version.cjs
157
+
151
158
  echo "🔎 Type checking..."
152
159
  pnpm exec tsc --noEmit
153
160
 
@@ -169,7 +176,7 @@ npm pack --dry-run >/dev/null
169
176
 
170
177
  # ─── Commit + Push ────────────────────────────────────
171
178
  echo "📝 Creating local commit..."
172
- git add package.json package-lock.json
179
+ git add package.json package-lock.json electron/package.json electron/package-lock.json
173
180
  git commit -m "[agent] chore: preview v$VERSION" --allow-empty
174
181
 
175
182
  echo "⬆️ Pushing preview branch..."
@@ -129,6 +129,12 @@ fi
129
129
  VERSION=$(node -p "require('./package.json').version")
130
130
  echo "📌 New version: $VERSION"
131
131
 
132
+ # Desktop artifacts are named from the version COMMITTED in
133
+ # electron/package.json, and desktop-release.yml builds from a tag checkout --
134
+ # so this has to happen here, before the release commit, not at build time.
135
+ echo "🖥️ Syncing Electron version to $VERSION..."
136
+ node scripts/sync-electron-version.cjs
137
+
132
138
  # ─── Collect changelog ─────────────────────────────────
133
139
  if [ -n "$PREV_TAG" ]; then
134
140
  CHANGELOG=$(git log "$PREV_TAG"..HEAD --pretty=format:"- %s" --no-merges | head -50)
@@ -155,10 +161,15 @@ fi
155
161
 
156
162
  # ─── Commit + Tag + Push ──────────────────────────────
157
163
  echo "🏷️ Creating git tag v$VERSION..."
158
- git add package.json package-lock.json
164
+ git add package.json package-lock.json electron/package.json electron/package-lock.json
159
165
  git commit -m "[agent] chore: release v$VERSION" --allow-empty
160
166
  git tag "v$VERSION"
161
- git push origin main
167
+ # HEAD:main, not main. `git push origin main` pushes the LOCAL main branch,
168
+ # which is not necessarily the tree just built, committed and tagged: releases
169
+ # are cut from whatever branch is checked out, and a local main left behind at
170
+ # an older release would be published instead. Caught before a real release
171
+ # with local main at v2.2.7 while the release commit sat on dev.
172
+ git push origin HEAD:main
162
173
  git push origin "v$VERSION"
163
174
 
164
175
  # ─── GitHub Release with changelog ─────────────────────
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Keep electron/package.json's version equal to the root package.json's.
4
+ *
5
+ * Without this the two drifted freely: the root reached 2.2.7 while the
6
+ * Electron shell sat at 0.1.0 since its first commit, so every desktop release
7
+ * produced identically-named artifacts (cli-jaw-0.1.0-arm64.dmg) and the file
8
+ * you downloaded told you nothing about which release it came from.
9
+ *
10
+ * Why the version has to be COMMITTED rather than injected at build time:
11
+ * electron-builder's default artifact name is `${productName}-${version}-...`
12
+ * resolved from the packaged app's own package.json, and
13
+ * .github/workflows/desktop-release.yml builds from a release-tag checkout. A
14
+ * build-time flag would work on a developer machine and silently do nothing in
15
+ * the workflow that actually publishes the assets.
16
+ *
17
+ * One implementation serves both the writer and the gate on purpose. A gate
18
+ * that computed the expected value separately could pass while the release
19
+ * wrote something else.
20
+ */
21
+ const { readFileSync } = require('node:fs');
22
+ const { join } = require('node:path');
23
+ const { spawnSync } = require('node:child_process');
24
+
25
+ const repoRoot = join(__dirname, '..');
26
+ const electronDir = join(repoRoot, 'electron');
27
+
28
+ /** Version field of the package.json in `dir`. */
29
+ function readVersion(dir) {
30
+ const file = join(dir, 'package.json');
31
+ let raw;
32
+ try {
33
+ raw = readFileSync(file, 'utf8');
34
+ } catch (error) {
35
+ throw new Error(`cannot read ${file}: ${error.message}`);
36
+ }
37
+ try {
38
+ return JSON.parse(raw).version;
39
+ } catch (error) {
40
+ throw new Error(`${file} is not valid JSON: ${error.message}`);
41
+ }
42
+ }
43
+
44
+ function main() {
45
+ let rootVersion;
46
+ let electronVersion;
47
+ try {
48
+ rootVersion = readVersion(repoRoot);
49
+ electronVersion = readVersion(electronDir);
50
+ } catch (error) {
51
+ // Fails closed either way -- an unreadable manifest already exited
52
+ // non-zero via the uncaught throw. What this adds is a first line the
53
+ // reader can act on instead of a Node stack trace.
54
+ console.error(`ERROR: ${error.message}`);
55
+ return 1;
56
+ }
57
+ const check = process.argv.includes('--check');
58
+
59
+ if (electronVersion === rootVersion) {
60
+ console.log(`[electron-version] OK ${rootVersion}`);
61
+ return 0;
62
+ }
63
+
64
+ if (check) {
65
+ // Name both values. "versions differ" leaves the reader guessing which
66
+ // side is authoritative.
67
+ console.error(
68
+ `ERROR: electron/package.json version ${electronVersion} != root package.json ${rootVersion}`);
69
+ console.error('Run: npm run sync:electron-version');
70
+ return 1;
71
+ }
72
+
73
+ // Delegate to npm instead of editing JSON by hand: `npm version` updates
74
+ // package.json and the root entries of package-lock.json together. Writing
75
+ // both files directly means reimplementing the lockfile's shape, and
76
+ // getting it subtly wrong surfaces later as unexplained churn in someone
77
+ // else's dependency diff.
78
+ const result = spawnSync(
79
+ 'npm',
80
+ ['version', rootVersion, '--no-git-tag-version', '--allow-same-version'],
81
+ { cwd: electronDir, encoding: 'utf8', timeout: 60_000 },
82
+ );
83
+ if (result.status !== 0) {
84
+ const detail = (result.stderr || result.stdout || '').trim();
85
+ console.error(`ERROR: npm version failed in electron/: ${detail}`);
86
+ return 1;
87
+ }
88
+
89
+ console.log(`[electron-version] synced ${electronVersion} -> ${rootVersion}`);
90
+ return 0;
91
+ }
92
+
93
+ process.exit(main());
@@ -1 +0,0 @@
1
- import{r as e}from"./rolldown-runtime-XQCOJYun.js";import{n as t,t as n}from"./jsx-runtime-gFMgZ0TQ.js";import{a as r,i,n as a,r as o,t as s}from"./page-shell-LF7vVVfG.js";import{a as c,n as l,o as u,s as d}from"./fields-zkeByFQW.js";import{t as f}from"./path-utils-LsJeDCKF.js";import{a as p,n as m,r as h,t as g}from"./TransportStatusChips-Bld0yXi7.js";var _=e(t(),1),v=n(),y=[`channel`,`discord.enabled`,`discord.token`,`discord.guildId`,`discord.channelIds`,`discord.forwardAll`,`discord.allowBots`,`discord.mentionOnly`];function b(e){return e?/^\d{5,32}$/.test(e.trim()):!1}function x({port:e,client:t,dirty:n,registerSave:x}){let{state:S,refresh:C,setData:w}=r(t,`/api/settings`),[T,E]=(0,_.useState)(!1),[D,O]=(0,_.useState)(``),[k,A]=(0,_.useState)(``),[j,M]=(0,_.useState)([]),[N,P]=(0,_.useState)(!0),[F,I]=(0,_.useState)(!1),[L,R]=(0,_.useState)(!1);(0,_.useEffect)(()=>{if(S.kind!==`ready`)return;let e=S.data.discord||{};E(!!e.enabled),O(``),A(e.guildId??``),M(Array.isArray(e.channelIds)?[...e.channelIds]:[]),P(e.forwardAll!==!1),I(!!e.allowBots),R(!!e.mentionOnly)},[S]),(0,_.useEffect)(()=>()=>{for(let e of y)n.remove(e)},[n]);let z=(0,_.useCallback)((e,t)=>n.set(e,t),[n]),B=(0,_.useMemo)(()=>S.kind===`ready`&&S.data.discord||{},[S]),V=S.kind===`ready`?S.data.channel:void 0,H=(0,_.useCallback)(async()=>{let e=n.saveBundle();if(Object.keys(e).length===0)return;let r=f(e),i=await t.put(`/api/settings`,r),a=i&&typeof i==`object`&&`data`in i?i.data:i;n.clear(),w(a);let o=a.discord||{};E(!!o.enabled),O(``),A(o.guildId??``),M(Array.isArray(o.channelIds)?[...o.channelIds]:[]),P(o.forwardAll!==!1),I(!!o.allowBots),R(!!o.mentionOnly),await C()},[t,n,C,w]);if((0,_.useEffect)(()=>{if(x)return x(H),()=>x(null)},[x,H]),S.kind===`loading`)return(0,v.jsx)(a,{});if(S.kind===`offline`)return(0,v.jsx)(o,{port:e});if(S.kind===`error`)return(0,v.jsx)(s,{message:S.message});let U=B.token?B.token.slice(-4):``,W=B.token?`••••••••${U}`:`(empty)`,G=k&&!b(k)?`Guild ID must be a numeric snowflake.`:null,K=j.filter(e=>!b(e)),q=K.length>0?`Snowflake IDs only — invalid: ${K.join(`, `)}`:null;return(0,v.jsxs)(`form`,{className:`settings-page-form`,onSubmit:e=>{e.preventDefault(),H()},children:[(0,v.jsxs)(i,{title:`Channels`,hint:`Choose which channel receives inbound chat. Outbound send can still work on the other channel when configured.`,children:[(0,v.jsx)(p,{original:V,dirty:n,idPrefix:`dc-channel`}),(0,v.jsx)(g,{client:t,channel:`discord`})]}),(0,v.jsxs)(i,{title:`Discord`,hint:`Bot token, guild + channels, forwarding rules.`,children:[(0,v.jsx)(c,{id:`dc-enabled`,label:`Discord enabled`,value:T,onChange:e=>{E(e),z(`discord.enabled`,{value:e,original:!!B.enabled,valid:!0})}}),(0,v.jsx)(u,{id:`dc-token`,label:`Bot token`,value:D,placeholder:W,onChange:e=>{if(O(e),e.length===0){n.remove(`discord.token`);return}z(`discord.token`,{value:e,original:B.token??``,valid:!0})}}),(0,v.jsx)(d,{id:`dc-guildId`,label:`Guild ID`,value:k,placeholder:`123456789012345678`,error:G,onChange:e=>{A(e);let t=e.length===0||b(e);z(`discord.guildId`,{value:e,original:B.guildId??``,valid:t})}}),(0,v.jsx)(l,{id:`dc-channelIds`,label:`Channel IDs`,value:j,placeholder:`987654321098765432`,error:q,onChange:e=>{M(e);let t=e.every(b);z(`discord.channelIds`,{value:e,original:B.channelIds??[],valid:t})}}),(0,v.jsx)(c,{id:`dc-forwardAll`,label:`Forward all`,value:N,onChange:e=>{P(e),z(`discord.forwardAll`,{value:e,original:B.forwardAll!==!1,valid:!0})}}),(0,v.jsx)(c,{id:`dc-allowBots`,label:`Allow other bots`,value:F,onChange:e=>{I(e),z(`discord.allowBots`,{value:e,original:!!B.allowBots,valid:!0})}}),(0,v.jsx)(c,{id:`dc-mentionOnly`,label:`Mention only`,value:L,onChange:e=>{R(e),z(`discord.mentionOnly`,{value:e,original:!!B.mentionOnly,valid:!0})}})]}),(0,v.jsx)(i,{title:`Health`,hint:`Probes /api/health for Discord readiness. Degraded mode means the bot is missing the MESSAGE_CONTENT intent (slash commands only).`,children:(0,v.jsx)(m,{client:t,label:`Discord`,endpoint:`/api/health`,method:`GET`,interpret:h})})]})}export{x as default,b as isValidSnowflake};
@@ -1 +0,0 @@
1
- import{r as e}from"./rolldown-runtime-XQCOJYun.js";import{n as t,t as n}from"./jsx-runtime-gFMgZ0TQ.js";import{a as r,i,n as a,r as o,t as s}from"./page-shell-LF7vVVfG.js";import{a as c,n as l,o as u}from"./fields-zkeByFQW.js";import{t as d}from"./path-utils-LsJeDCKF.js";import{a as f,i as p,n as m,t as h}from"./TransportStatusChips-Bld0yXi7.js";var g=e(t(),1),_=n(),v=[`channel`,`telegram.enabled`,`telegram.token`,`telegram.allowedChatIds`,`telegram.forwardAll`,`telegram.mentionOnly`];function y(e){return e?/^-?\d+$/.test(e.trim()):!1}function b(e){let t=[],n=[];for(let r of e)y(r)?t.push(r.trim()):n.push(r);return{valid:t,invalid:n}}function x(e){return e?e.filter(e=>Number.isFinite(e)).map(e=>String(e)):[]}function S(e){let t=[];for(let n of e){if(!y(n))continue;let e=Number(n);Number.isFinite(e)&&t.push(e)}return t}function C({port:e,client:t,dirty:n,registerSave:C}){let{state:w,refresh:T,setData:E}=r(t,`/api/settings`),[D,O]=(0,g.useState)(!1),[k,A]=(0,g.useState)(``),[j,M]=(0,g.useState)([]),[N,P]=(0,g.useState)(!0),[F,I]=(0,g.useState)(!0);(0,g.useEffect)(()=>{if(w.kind!==`ready`)return;let e=w.data.telegram||{};O(!!e.enabled),A(``),M(x(e.allowedChatIds)),P(e.forwardAll!==!1),I(e.mentionOnly!==!1)},[w]),(0,g.useEffect)(()=>()=>{for(let e of v)n.remove(e)},[n]);let L=(0,g.useCallback)((e,t)=>n.set(e,t),[n]),R=(0,g.useMemo)(()=>w.kind===`ready`&&w.data.telegram||{},[w]),z=w.kind===`ready`?w.data.channel:void 0,B=(0,g.useCallback)(async()=>{let e=n.saveBundle();if(Object.keys(e).length===0)return;let r=d(e),i=await t.put(`/api/settings`,r),a=i&&typeof i==`object`&&`data`in i?i.data:i;n.clear(),E(a);let o=a.telegram||{};O(!!o.enabled),A(``),M(x(o.allowedChatIds)),P(o.forwardAll!==!1),I(o.mentionOnly!==!1),await T()},[t,n,T,E]);if((0,g.useEffect)(()=>{if(C)return C(B),()=>C(null)},[C,B]),w.kind===`loading`)return(0,_.jsx)(a,{});if(w.kind===`offline`)return(0,_.jsx)(o,{port:e});if(w.kind===`error`)return(0,_.jsx)(s,{message:w.message});let V=R.token?R.token.slice(-4):``,H=R.token?`••••••••${V}`:`(empty)`,{invalid:U}=b(j),W=U.length>0?`Numeric chat IDs only — invalid: ${U.join(`, `)}`:null;return(0,_.jsxs)(`form`,{className:`settings-page-form`,onSubmit:e=>{e.preventDefault(),B()},children:[(0,_.jsxs)(i,{title:`Channels`,hint:`Choose which channel receives inbound chat. Outbound send can still work on the other channel when configured.`,children:[(0,_.jsx)(f,{original:z,dirty:n,idPrefix:`tg-channel`}),(0,_.jsx)(h,{client:t,channel:`telegram`})]}),(0,_.jsxs)(i,{title:`Telegram`,hint:`Bot token, allow-list, and forwarding rules.`,children:[(0,_.jsx)(c,{id:`tg-enabled`,label:`Telegram enabled`,value:D,onChange:e=>{O(e),L(`telegram.enabled`,{value:e,original:!!R.enabled,valid:!0})}}),(0,_.jsx)(u,{id:`tg-token`,label:`Bot token`,value:k,placeholder:H,onChange:e=>{if(A(e),e.length===0){n.remove(`telegram.token`);return}L(`telegram.token`,{value:e,original:R.token??``,valid:!0})}}),(0,_.jsx)(l,{id:`tg-allowedChatIds`,label:`Allowed chat IDs`,value:j,placeholder:`123456789`,error:W,onChange:e=>{M(e);let t=e.every(y);L(`telegram.allowedChatIds`,{value:S(e),original:R.allowedChatIds??[],valid:t})}}),(0,_.jsx)(c,{id:`tg-forwardAll`,label:`Forward all responses`,value:N,onChange:e=>{P(e),L(`telegram.forwardAll`,{value:e,original:R.forwardAll!==!1,valid:!0})}}),(0,_.jsx)(c,{id:`tg-mentionOnly`,label:`Mention only`,value:F,onChange:e=>{I(e),L(`telegram.mentionOnly`,{value:e,original:R.mentionOnly!==!1,valid:!0})}})]}),(0,_.jsx)(i,{title:`Health`,hint:`Probe the bot to confirm token + connectivity. 404 means the instance hasn't shipped the probe endpoint yet.`,children:(0,_.jsx)(m,{client:t,label:`Telegram`,endpoint:`/api/telegram/probe`,method:`POST`,interpret:p})})]})}export{x as chatIdsToChips,S as chipsToChatIds,C as default,y as isValidChatId,b as partitionChatIds};
@@ -1 +0,0 @@
1
- import{r as e}from"./rolldown-runtime-XQCOJYun.js";import{n as t,t as n}from"./jsx-runtime-gFMgZ0TQ.js";import{t as r}from"./settings-client-CjbBShxm.js";var i=e(t(),1),a=n(),o=[{value:`telegram`,label:`Telegram`},{value:`discord`,label:`Discord`}];function s({original:e,dirty:t,idPrefix:n=`channel`}){let[r,s]=(0,i.useState)(e??`telegram`),c=(0,i.useCallback)(n=>{s(n),t.set(`channel`,{value:n,original:e??`telegram`,valid:!0})},[t,e]);return(0,a.jsxs)(`fieldset`,{className:`settings-field settings-field-active-channel`,"aria-label":`Active inbound channel`,children:[(0,a.jsx)(`legend`,{className:`settings-field-label`,children:`Active inbound channel (chat replies)`}),(0,a.jsx)(`div`,{className:`settings-active-channel-options`,role:`radiogroup`,children:o.map(e=>{let t=`${n}-${e.value}`,i=r===e.value;return(0,a.jsxs)(`label`,{htmlFor:t,className:`settings-radio`,children:[(0,a.jsx)(`input`,{id:t,type:`radio`,name:n,value:e.value,checked:i,onChange:()=>c(e.value)}),(0,a.jsx)(`span`,{children:e.label})]},e.value)})})]})}function c({client:e,label:t,endpoint:n,method:o=`GET`,interpret:s,auto:c=!1}){let[d,f]=(0,i.useState)({kind:`idle`}),p=(0,i.useRef)(null),m=(0,i.useRef)(!0),h=(0,i.useCallback)(async()=>{p.current?.abort();let t=new AbortController;p.current=t,f({kind:`checking`});try{let r=o===`POST`?await e.post(n,{},{signal:t.signal}):await e.get(n,{signal:t.signal});if(!m.current||t.signal.aborted)return;f(s(r))}catch(e){if(!m.current||t.signal.aborted)return;if(e instanceof r&&e.status===404){f({kind:`unknown`,reason:`Probe endpoint not implemented on this instance.`});return}f({kind:`error`,reason:e instanceof Error?e.message:String(e)})}},[e,n,s,o]);return(0,i.useEffect)(()=>(m.current=!0,c&&h(),()=>{m.current=!1,p.current?.abort()}),[c,h]),(0,a.jsxs)(`div`,{className:`settings-health-badge`,role:`status`,"aria-live":`polite`,children:[(0,a.jsx)(`button`,{type:`button`,className:`settings-action settings-action-discard`,onClick:()=>void h(),disabled:d.kind===`checking`,children:d.kind===`checking`?`Checking…`:`Check ${t}`}),(0,a.jsx)(`span`,{className:`settings-health-pill is-${d.kind}`,children:l(d)}),u(d)?(0,a.jsx)(`span`,{className:`settings-health-detail`,children:u(d)}):null]})}function l(e){switch(e.kind){case`idle`:return`Not checked`;case`checking`:return`Checking`;case`ok`:return`Healthy`;case`degraded`:return`Degraded`;case`unknown`:return`Unknown`;case`error`:return`Error`}}function u(e){return e.kind===`ok`&&e.detail||e.kind===`degraded`?e.detail:e.kind===`unknown`||e.kind===`error`?e.reason:null}function d(e){if(!e||typeof e!=`object`)return{kind:`unknown`,reason:`Empty probe response.`};let t=e;if(t.ok===!0){let e=typeof t.username==`string`?t.username:void 0;return e?{kind:`ok`,detail:`@${e}`}:{kind:`ok`}}return{kind:`error`,reason:typeof t.error==`string`?t.error:`Probe failed.`}}function f(e){if(!e||typeof e!=`object`)return{kind:`unknown`,reason:`Empty health response.`};let t=e,n=t.discord&&typeof t.discord==`object`?t.discord:null;return n?n.ready===!0&&n.degraded!==!0?{kind:`ok`}:n.degraded===!0?{kind:`degraded`,detail:typeof n.degradedReason==`string`?n.degradedReason:`MESSAGE_CONTENT intent missing — slash commands only.`}:n.ready===!1?{kind:`error`,reason:typeof n.error==`string`?n.error:`Bot not connected.`}:{kind:`unknown`,reason:`Discord status indeterminate.`}:{kind:`unknown`,reason:`No discord block in /api/health.`}}function p(e){if(!e||typeof e!=`object`)return!1;let t=e;return typeof t.configured==`boolean`&&typeof t.activeInbound==`boolean`&&typeof t.sendCapable==`boolean`}function m(e){if(!e||typeof e!=`object`)return null;let t=e.channels;if(!t||typeof t!=`object`)return null;let n=t;return n.activeInbound!==`telegram`&&n.activeInbound!==`discord`||!p(n.telegram)||!p(n.discord)?null:{activeInbound:n.activeInbound,telegram:n.telegram,discord:n.discord}}function h(e){let t=[];return t.push(e.configured?`Configured`:`Not configured`),e.sendCapable&&t.push(`Send-capable`),e.activeInbound&&t.push(`Active inbound`),t}function g({client:e,channel:t}){let[n,r]=(0,i.useState)(null),[o,s]=(0,i.useState)(null),c=(0,i.useRef)(!0),l=(0,i.useCallback)(async()=>{try{let t=await e.get(`/api/health`);if(!c.current)return;let n=m(t);r(n),s(n?null:`Channel health unavailable on this instance.`)}catch(e){if(!c.current)return;r(null),s(e instanceof Error?e.message:String(e))}},[e]);(0,i.useEffect)(()=>(c.current=!0,l(),()=>{c.current=!1}),[l]);let u=n?.[t]??null,d=!!(u?.configured&&u.sendCapable&&!u.activeInbound);return(0,a.jsxs)(`div`,{className:`settings-transport-status`,role:`status`,"aria-live":`polite`,children:[o?(0,a.jsx)(`p`,{className:`settings-field-hint`,children:o}):null,u?(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(`div`,{className:`settings-transport-chips`,children:h(u).map(e=>(0,a.jsx)(`span`,{className:`settings-health-pill is-ok`,children:e},e))}),d?(0,a.jsx)(`p`,{className:`settings-field-hint`,children:`This transport is not receiving inbound messages, but send-only delivery remains available.`}):null]}):null,(0,a.jsx)(`button`,{type:`button`,className:`settings-action settings-action-discard`,onClick:()=>void l(),children:`Refresh transport status`})]})}export{s as a,d as i,c as n,f as r,g as t};