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.
- package/README.md +4 -4
- package/dist/bin/agent-driven.js +33 -0
- package/dist/bin/agent-driven.js.map +1 -0
- package/dist/bin/commands/doctor.js +66 -1
- package/dist/bin/commands/doctor.js.map +1 -1
- package/dist/bin/commands/init.js +107 -8
- package/dist/bin/commands/init.js.map +1 -1
- package/dist/bin/interactive-confirm.js +110 -0
- package/dist/bin/interactive-confirm.js.map +1 -0
- package/dist/bin/star-prompt.js +46 -15
- package/dist/bin/star-prompt.js.map +1 -1
- package/dist/lib/upload.js +5 -1
- package/dist/lib/upload.js.map +1 -1
- package/dist/server.js +1 -0
- package/dist/server.js.map +1 -1
- package/dist/src/agent/alert-escalation.js +1 -1
- package/dist/src/agent/alert-escalation.js.map +1 -1
- package/dist/src/agent/codex-app-catalog.js +57 -0
- package/dist/src/agent/codex-app-catalog.js.map +1 -0
- package/dist/src/agent/codex-app-client.js +89 -7
- package/dist/src/agent/codex-app-client.js.map +1 -1
- package/dist/src/agent/pi-rpc-verdict.js +22 -0
- package/dist/src/agent/pi-rpc-verdict.js.map +1 -0
- package/dist/src/agent/pi-runtime.js +240 -0
- package/dist/src/agent/pi-runtime.js.map +1 -1
- package/dist/src/agent/runtime-pool.js +445 -0
- package/dist/src/agent/runtime-pool.js.map +1 -0
- package/dist/src/agent/spawn.js +395 -240
- package/dist/src/agent/spawn.js.map +1 -1
- package/dist/src/cli/command-context.js +5 -2
- package/dist/src/cli/command-context.js.map +1 -1
- package/dist/src/cli/commands.js +43 -39
- package/dist/src/cli/commands.js.map +1 -1
- package/dist/src/cli/handlers-runtime.js +15 -7
- package/dist/src/cli/handlers-runtime.js.map +1 -1
- package/dist/src/cli/handlers-search.js +1 -1
- package/dist/src/cli/handlers-search.js.map +1 -1
- package/dist/src/cli/handlers-skill-invoke.js +1 -1
- package/dist/src/cli/handlers-skill-invoke.js.map +1 -1
- package/dist/src/cli/handlers-workflows.js +1 -1
- package/dist/src/cli/handlers-workflows.js.map +1 -1
- package/dist/src/cli/handlers.js +1 -1
- package/dist/src/cli/handlers.js.map +1 -1
- package/dist/src/cli/opencodex-models.js +40 -7
- package/dist/src/cli/opencodex-models.js.map +1 -1
- package/dist/src/cli/registry-live.js +6 -5
- package/dist/src/cli/registry-live.js.map +1 -1
- package/dist/src/command-contract/catalog.js +4 -0
- package/dist/src/command-contract/catalog.js.map +1 -1
- package/dist/src/core/config.js +69 -4
- package/dist/src/core/config.js.map +1 -1
- package/dist/src/core/runtime-settings.js +17 -0
- package/dist/src/core/runtime-settings.js.map +1 -1
- package/dist/src/core/settings-merge.js +1 -1
- package/dist/src/core/settings-merge.js.map +1 -1
- package/dist/src/discord/bot.js +29 -14
- package/dist/src/discord/bot.js.map +1 -1
- package/dist/src/discord/commands.js +5 -4
- package/dist/src/discord/commands.js.map +1 -1
- package/dist/src/discord/discord-file.js +8 -2
- package/dist/src/discord/discord-file.js.map +1 -1
- package/dist/src/discord/forwarder.js +17 -19
- package/dist/src/discord/forwarder.js.map +1 -1
- package/dist/src/discord/send-only-client.js +9 -1
- package/dist/src/discord/send-only-client.js.map +1 -1
- package/dist/src/manager/dashboard-service.js +38 -1
- package/dist/src/manager/dashboard-service.js.map +1 -1
- package/dist/src/manager/telegram-hub/hub-bot.js +47 -29
- package/dist/src/manager/telegram-hub/hub-bot.js.map +1 -1
- package/dist/src/messaging/channel-health.js +32 -0
- package/dist/src/messaging/channel-health.js.map +1 -1
- package/dist/src/messaging/chunk.js +376 -0
- package/dist/src/messaging/chunk.js.map +1 -0
- package/dist/src/messaging/dedupe.js +104 -0
- package/dist/src/messaging/dedupe.js.map +1 -0
- package/dist/src/messaging/fold.js +210 -0
- package/dist/src/messaging/fold.js.map +1 -0
- package/dist/src/messaging/redact.js +577 -0
- package/dist/src/messaging/redact.js.map +1 -0
- package/dist/src/messaging/retry.js +76 -0
- package/dist/src/messaging/retry.js.map +1 -0
- package/dist/src/messaging/runtime.js +6 -3
- package/dist/src/messaging/runtime.js.map +1 -1
- package/dist/src/messaging/send.js +45 -15
- package/dist/src/messaging/send.js.map +1 -1
- package/dist/src/messaging/slack-target.js +50 -0
- package/dist/src/messaging/slack-target.js.map +1 -0
- package/dist/src/messaging/types.js.map +1 -1
- package/dist/src/orchestrator/pipeline.js +4 -4
- package/dist/src/orchestrator/pipeline.js.map +1 -1
- package/dist/src/routes/command.js +11 -7
- package/dist/src/routes/command.js.map +1 -1
- package/dist/src/routes/messaging.js +28 -12
- package/dist/src/routes/messaging.js.map +1 -1
- package/dist/src/routes/settings.js +22 -6
- package/dist/src/routes/settings.js.map +1 -1
- package/dist/src/slack/api.js +116 -0
- package/dist/src/slack/api.js.map +1 -0
- package/dist/src/slack/bot.js +262 -0
- package/dist/src/slack/bot.js.map +1 -0
- package/dist/src/slack/commands.js +98 -0
- package/dist/src/slack/commands.js.map +1 -0
- package/dist/src/slack/events.js +116 -0
- package/dist/src/slack/events.js.map +1 -0
- package/dist/src/slack/format.js +58 -0
- package/dist/src/slack/format.js.map +1 -0
- package/dist/src/slack/forwarder.js +57 -0
- package/dist/src/slack/forwarder.js.map +1 -0
- package/dist/src/slack/register.js +13 -0
- package/dist/src/slack/register.js.map +1 -0
- package/dist/src/slack/send-handler.js +41 -0
- package/dist/src/slack/send-handler.js.map +1 -0
- package/dist/src/slack/send-only-client.js +46 -0
- package/dist/src/slack/send-only-client.js.map +1 -0
- package/dist/src/slack/slack-file.js +72 -0
- package/dist/src/slack/slack-file.js.map +1 -0
- package/dist/src/slack/socket.js +341 -0
- package/dist/src/slack/socket.js.map +1 -0
- package/dist/src/telegram/bot.js +117 -37
- package/dist/src/telegram/bot.js.map +1 -1
- package/dist/src/telegram/elicitation-buttons.js +7 -2
- package/dist/src/telegram/elicitation-buttons.js.map +1 -1
- package/dist/src/telegram/forwarder.js +15 -7
- package/dist/src/telegram/forwarder.js.map +1 -1
- package/dist/src/telegram/rich-message.js +209 -24
- package/dist/src/telegram/rich-message.js.map +1 -1
- package/dist/src/telegram/telegram-file.js +35 -4
- package/dist/src/telegram/telegram-file.js.map +1 -1
- package/dist/src/telegram/voice.js +4 -3
- package/dist/src/telegram/voice.js.map +1 -1
- package/package.json +5 -1
- package/public/assets/providers/slack.svg +1 -0
- package/public/dist/assets/ChannelsDiscord-D9FLnZso.js +1 -0
- package/public/dist/assets/ChannelsSlack-ybvupgdK.js +1 -0
- package/public/dist/assets/ChannelsTelegram-BOCOPrBc.js +1 -0
- package/public/dist/assets/HealthBadge-megTlF4n.js +1 -0
- package/public/dist/assets/TransportStatusChips-CjEySMis.js +1 -0
- package/public/dist/assets/app-BGIAy4he.js +55 -0
- package/public/dist/assets/{bgtask-badge-CdBqBy1L.js → bgtask-badge-DXca8E7c.js} +1 -1
- package/public/dist/assets/bgtask-badge-GRSiNILQ.js +1 -0
- package/public/dist/assets/{employees-C8qufeJl.js → employees-BL4qMxJj.js} +1 -1
- package/public/dist/assets/iframe-renderer-BUSk0Rw9.js +1 -0
- package/public/dist/assets/{iframe-renderer-CZ67aeTO.js → iframe-renderer-DUI0LYBy.js} +2 -2
- package/public/dist/assets/{manager-CLXT60M5.js → manager-B-gpf-3j.js} +3 -3
- package/public/dist/assets/{memory-lWramLVw.js → memory-B_Y0L0KD.js} +1 -1
- package/public/dist/assets/memory-CX6YahTc.js +1 -0
- package/public/dist/assets/message-history-Det7u76S.js +1 -0
- package/public/dist/assets/{message-history-2NUN3UxM.js → message-history-Dl8dJzez.js} +1 -1
- package/public/dist/assets/{provider-icons-stsNmQKx.js → provider-icons-VDfahiUp.js} +3 -2
- package/public/dist/assets/{render-CsF-ojLe.js → render-BI1aqKoc.js} +1 -1
- package/public/dist/assets/{settings-BMLowRCh.js → settings-B0Sj4sUH.js} +1 -1
- package/public/dist/assets/settings-C7NutP6X.js +1 -0
- package/public/dist/assets/settings-core-CyX-px6_.js +100 -0
- package/public/dist/assets/settings-core-DVpFmrhY.js +1 -0
- package/public/dist/assets/{sidebar-gKjYLv49.js → sidebar-DOR9fTgN.js} +3 -3
- package/public/dist/assets/skills-BWhKreQ2.js +1 -0
- package/public/dist/assets/{skills-CKTbomdm.js → skills-wt0fkV9Y.js} +1 -1
- package/public/dist/assets/slash-commands-BZbqTg3v.js +20 -0
- package/public/dist/assets/slash-commands-Di4NDA4w.js +1 -0
- package/public/dist/assets/{trace-drawer-C3G4mmB1.js → trace-drawer-kMYzaVFW.js} +1 -1
- package/public/dist/assets/ui-BghexWrB.js +1 -0
- package/public/dist/assets/{ui-CVVC_flX.js → ui-D4D5HUfo.js} +1 -1
- package/public/dist/index.html +62 -1
- package/public/dist/manager/index.html +1 -1
- package/public/index.html +61 -0
- package/public/js/features/command-info.ts +1 -0
- package/public/js/features/help-content.ts +8 -0
- package/public/js/features/settings-channel.ts +5 -1
- package/public/js/features/settings-core.ts +2 -0
- package/public/js/features/settings-slack.ts +74 -0
- package/public/js/features/settings-types.ts +5 -1
- package/public/js/features/settings.ts +1 -0
- package/public/js/features/transport-status-row.ts +15 -3
- package/public/js/main.ts +19 -0
- package/public/js/provider-icons.ts +4 -1
- package/public/js/ws.ts +1 -1
- package/public/locales/en.json +21 -0
- package/public/locales/ja.json +21 -0
- package/public/locales/ko.json +21 -0
- package/public/locales/zh.json +21 -0
- package/public/manager/src/settings/SettingsShell.tsx +1 -0
- package/public/manager/src/settings/SettingsSidebar.tsx +1 -0
- package/public/manager/src/settings/pages/ChannelsSlack.tsx +323 -0
- package/public/manager/src/settings/pages/components/ActiveChannelToggle.tsx +2 -1
- package/public/manager/src/settings/pages/components/TransportStatusChips.tsx +13 -4
- package/public/manager/src/settings/types.ts +1 -0
- package/scripts/check-redaction-sinks.mjs +276 -0
- package/scripts/pi-rpc-probe.mts +361 -0
- package/scripts/release-gates.mjs +83 -0
- package/scripts/release-preview.sh +8 -1
- package/scripts/release.sh +13 -2
- package/scripts/sync-electron-version.cjs +93 -0
- package/public/dist/assets/ChannelsDiscord-Bt1_rtNC.js +0 -1
- package/public/dist/assets/ChannelsTelegram-BLV0m1oq.js +0 -1
- package/public/dist/assets/TransportStatusChips-Bld0yXi7.js +0 -1
- package/public/dist/assets/app-iSJhsP20.js +0 -55
- package/public/dist/assets/bgtask-badge-CSkSsoB6.js +0 -1
- package/public/dist/assets/iframe-renderer-DXdo0YXN.js +0 -1
- package/public/dist/assets/memory-DeKUlD2M.js +0 -1
- package/public/dist/assets/message-history-CWTQ2woA.js +0 -1
- package/public/dist/assets/settings-CDjs7hTT.js +0 -1
- package/public/dist/assets/settings-core-Cs_oq-b3.js +0 -99
- package/public/dist/assets/settings-core-D-TOh1a6.js +0 -1
- package/public/dist/assets/skills-GuIfNASs.js +0 -1
- package/public/dist/assets/slash-commands-BtZa093n.js +0 -20
- package/public/dist/assets/slash-commands-DGJKfpMT.js +0 -1
- package/public/dist/assets/ui-DJg5Q_NJ.js +0 -1
- /package/public/dist/assets/{DashboardMeta-B7CVqXcU.js → DashboardMeta-DennaaA_.js} +0 -0
- /package/public/dist/assets/{Display-BF-rACK6.js → Display-hGuSn_W5.js} +0 -0
- /package/public/dist/assets/{Employees-D3hnciP0.js → Employees-B5YXk5ma.js} +0 -0
- /package/public/dist/assets/{Heartbeat-D4Pn5O3O.js → Heartbeat-vDGhPfct.js} +0 -0
- /package/public/dist/assets/{Mcp-SfytKWGX.js → Mcp-BHWnJ6SP.js} +0 -0
- /package/public/dist/assets/{Memory-DNxBOQ7N.js → Memory-B_UZ-iCA.js} +0 -0
- /package/public/dist/assets/{ModelProvider-CXwtlG7l.js → ModelProvider-CgUvs6pE.js} +0 -0
- /package/public/dist/assets/{Network-DISUhige.js → Network-AWpSkALD.js} +0 -0
- /package/public/dist/assets/{Profile-B8yBO3Ju.js → Profile-DnIE2ios.js} +0 -0
- /package/public/dist/assets/{Prompts-5lNQ5qOm.js → Prompts-B9aKLall.js} +0 -0
- /package/public/dist/assets/{SpeechKeys-BepaIHBa.js → SpeechKeys-K4Be_o7z.js} +0 -0
- /package/public/dist/assets/{TelegramHub--DNjRAwW.js → TelegramHub-CFwSkzHP.js} +0 -0
- /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..."
|
package/scripts/release.sh
CHANGED
|
@@ -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};
|