@0xmaxma/claude-gateway 1.3.24 → 1.3.31
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 +172 -50
- package/config.template.json +6 -2
- package/dist/agent/incident-store.d.ts +89 -0
- package/dist/agent/incident-store.d.ts.map +1 -0
- package/dist/agent/incident-store.js +299 -0
- package/dist/agent/incident-store.js.map +1 -0
- package/dist/agent/incident.d.ts +156 -0
- package/dist/agent/incident.d.ts.map +1 -0
- package/dist/agent/incident.js +177 -0
- package/dist/agent/incident.js.map +1 -0
- package/dist/agent/recovery-executor.d.ts +117 -0
- package/dist/agent/recovery-executor.d.ts.map +1 -0
- package/dist/agent/recovery-executor.js +168 -0
- package/dist/agent/recovery-executor.js.map +1 -0
- package/dist/agent/recovery-policy.d.ts +97 -0
- package/dist/agent/recovery-policy.d.ts.map +1 -0
- package/dist/agent/recovery-policy.js +164 -0
- package/dist/agent/recovery-policy.js.map +1 -0
- package/dist/agent/runner.d.ts +44 -0
- package/dist/agent/runner.d.ts.map +1 -1
- package/dist/agent/runner.js +272 -2
- package/dist/agent/runner.js.map +1 -1
- package/dist/agent/safe-mode.d.ts +61 -0
- package/dist/agent/safe-mode.d.ts.map +1 -0
- package/dist/agent/safe-mode.js +102 -0
- package/dist/agent/safe-mode.js.map +1 -0
- package/dist/agent/triage.d.ts +94 -0
- package/dist/agent/triage.d.ts.map +1 -0
- package/dist/agent/triage.js +209 -0
- package/dist/agent/triage.js.map +1 -0
- package/dist/agent/turn-trace.d.ts +120 -0
- package/dist/agent/turn-trace.d.ts.map +1 -0
- package/dist/agent/turn-trace.js +122 -0
- package/dist/agent/turn-trace.js.map +1 -0
- package/dist/api/gateway-router.d.ts +21 -0
- package/dist/api/gateway-router.d.ts.map +1 -1
- package/dist/api/gateway-router.js +58 -17
- package/dist/api/gateway-router.js.map +1 -1
- package/dist/api/line-pending-senders.d.ts +7 -2
- package/dist/api/line-pending-senders.d.ts.map +1 -1
- package/dist/api/line-pending-senders.js +7 -2
- package/dist/api/line-pending-senders.js.map +1 -1
- package/dist/api/router.d.ts.map +1 -1
- package/dist/api/router.js +563 -172
- package/dist/api/router.js.map +1 -1
- package/dist/api/wizard-state.d.ts +1 -4
- package/dist/api/wizard-state.d.ts.map +1 -1
- package/dist/api/wizard-state.js.map +1 -1
- package/dist/config/migrator.d.ts +4 -0
- package/dist/config/migrator.d.ts.map +1 -1
- package/dist/config/migrator.js +60 -3
- package/dist/config/migrator.js.map +1 -1
- package/dist/discord/receiver.d.ts +1 -0
- package/dist/discord/receiver.d.ts.map +1 -1
- package/dist/discord/receiver.js +24 -6
- package/dist/discord/receiver.js.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/session/process.d.ts +18 -0
- package/dist/session/process.d.ts.map +1 -1
- package/dist/session/process.js +61 -1
- package/dist/session/process.js.map +1 -1
- package/dist/shell/claude-pty-shell.js +59 -0
- package/dist/shell/claude-pty-shell.js.map +1 -1
- package/dist/shell/control-channel.d.ts +74 -0
- package/dist/shell/control-channel.d.ts.map +1 -0
- package/dist/shell/control-channel.js +114 -0
- package/dist/shell/control-channel.js.map +1 -0
- package/dist/telegram/receiver.d.ts +1 -0
- package/dist/telegram/receiver.d.ts.map +1 -1
- package/dist/telegram/receiver.js +18 -6
- package/dist/telegram/receiver.js.map +1 -1
- package/dist/types.d.ts +21 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/web-ui.d.ts.map +1 -1
- package/dist/ui/web-ui.js +103 -2
- package/dist/ui/web-ui.js.map +1 -1
- package/mcp/tools/discord/access.ts +136 -20
- package/mcp/tools/discord/client.ts +3 -1
- package/mcp/tools/discord/module.ts +75 -4
- package/mcp/tools/discord/skills/access/SKILL.md +66 -8
- package/mcp/tools/discord/skills/configure/SKILL.md +11 -1
- package/mcp/tools/discord/types.ts +21 -2
- package/mcp/tools/skills/handlers.ts +4 -2
- package/mcp/tools/telegram/dedup.ts +4 -1
- package/mcp/tools/telegram/module.ts +5 -14
- package/mcp/tools/telegram/pure.ts +169 -31
- package/mcp/tools/telegram/receiver-server.ts +271 -78
- package/mcp/tools/telegram/skills/access/SKILL.md +93 -23
- package/mcp/tools/telegram/skills/configure/SKILL.md +31 -26
- package/mcp/tools/telegram/typing.ts +124 -0
- package/package.json +3 -1
|
@@ -51,46 +51,44 @@ Read both state files and give the user a complete picture:
|
|
|
51
51
|
(`123456789:...`).
|
|
52
52
|
|
|
53
53
|
2. **Access** — read `{STATE_DIR}/access.json` (missing file
|
|
54
|
-
= defaults: `dmPolicy: "
|
|
55
|
-
- DM policy and what it means in one line
|
|
54
|
+
= defaults: `dmPolicy: "allowlist"`, `pairing: true`, empty allowlist). Show:
|
|
55
|
+
- DM policy (`open`/`allowlist`/`disabled`) and what it means in one line
|
|
56
|
+
- Pairing toggle (on/off) and what it means in one line
|
|
56
57
|
- Allowed senders: count, and list display names or IDs
|
|
57
58
|
- Pending pairings: count, with codes and display names if any
|
|
58
59
|
|
|
59
60
|
3. **What next** — end with a concrete next step based on state:
|
|
60
61
|
- No token → *"Run `/telegram:configure <token>` with the token from
|
|
61
62
|
BotFather."*
|
|
62
|
-
- Token set,
|
|
63
|
-
Telegram. It replies with a code
|
|
64
|
-
<code
|
|
63
|
+
- Token set, `allowlist` + pairing on, nobody allowed → *"DM your bot on
|
|
64
|
+
Telegram. It replies with a code — approve it from the web Channels card
|
|
65
|
+
(or run `/telegram:access pair <code>` here)."*
|
|
65
66
|
- Token set, someone allowed → *"Ready. DM your bot to reach the
|
|
66
67
|
assistant."*
|
|
67
68
|
|
|
68
|
-
**
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
**The access model.** The base policy should be `allowlist` — only people on
|
|
70
|
+
the list reach the assistant. **Pairing** is an orthogonal on/off toggle
|
|
71
|
+
(default on) that sits on top of `allowlist`: when on, an unknown sender who
|
|
72
|
+
DMs the bot gets a one-time code that shows up in Pending, and the admin
|
|
73
|
+
approves it (from the web Channels card, or `/telegram:access pair <code>`).
|
|
74
|
+
It's a lightweight identity check, and it's fine to leave on as your standing
|
|
75
|
+
way to let new people in. Turn pairing **off** only if you want a hard
|
|
76
|
+
allowlist where strangers are dropped silently with no code.
|
|
72
77
|
|
|
73
78
|
Drive the conversation this way:
|
|
74
79
|
|
|
75
80
|
1. Read the allowlist. Tell the user who's in it.
|
|
76
81
|
2. Ask: *"Is that everyone who should reach you through this bot?"*
|
|
77
|
-
3. **If
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
6. **If policy is already `allowlist`** → confirm this is the locked state.
|
|
88
|
-
If they need to add someone: *"They'll need to give you their numeric ID
|
|
89
|
-
(have them message @userinfobot), or you can briefly flip to pairing:
|
|
90
|
-
`/telegram:access policy pairing` → they DM → you pair → flip back."*
|
|
91
|
-
|
|
92
|
-
Never frame `pairing` as the correct long-term choice. Don't skip the lockdown
|
|
93
|
-
offer.
|
|
82
|
+
3. **If someone's missing** → *"Leave pairing on: have them DM the bot, then
|
|
83
|
+
approve the code that appears in Pending (web card or `/telegram:access
|
|
84
|
+
pair <code>`)."*
|
|
85
|
+
4. **If the allowlist is empty and they haven't paired themselves yet** →
|
|
86
|
+
*"DM your bot to capture your own ID first — approve the code and you're
|
|
87
|
+
in."*
|
|
88
|
+
5. **If they want a hard lockdown** (no new codes for strangers) → offer to
|
|
89
|
+
run `/telegram:access pairing off`. This keeps `allowlist` but stops
|
|
90
|
+
minting codes; add people later by flipping pairing back on, or with
|
|
91
|
+
`/telegram:access allow <senderId>`.
|
|
94
92
|
|
|
95
93
|
### `<token>` — save it
|
|
96
94
|
|
|
@@ -116,3 +114,10 @@ Delete the `TELEGRAM_BOT_TOKEN=` line (or the file if that's the only line).
|
|
|
116
114
|
or `/reload-plugins`. Say so after saving.
|
|
117
115
|
- `access.json` is re-read on every inbound message — policy changes via
|
|
118
116
|
`/telegram:access` take effect immediately, no restart.
|
|
117
|
+
- **Groups need more than allowlisting.** DMs work over long-polling, but in a
|
|
118
|
+
group Telegram's default **Privacy Mode** stops the bot from receiving plain
|
|
119
|
+
messages (it only gets `/commands`, @mentions, and replies), and commands are
|
|
120
|
+
dropped in groups anyway. To use groups, tell the user to promote the bot to
|
|
121
|
+
**Admin** in the group (or disable Privacy Mode in BotFather and re-add it),
|
|
122
|
+
then allowlist the group via `/telegram:access` (pairing code or `group allow
|
|
123
|
+
<groupId>`).
|
|
@@ -13,6 +13,17 @@
|
|
|
13
13
|
* STATE_DIR/typing/{chatId}.error — written by AgentRunner on session failure
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
// Import compiled dist/, not raw src/ — src/ is not published (files: ["mcp/"]),
|
|
17
|
+
// so a src/ import crashes this bun-run tool on installed packages. Enforced by
|
|
18
|
+
// tests/unit/mcp-no-src-imports.test.ts.
|
|
19
|
+
import {
|
|
20
|
+
classifyTurn,
|
|
21
|
+
type TurnObservation,
|
|
22
|
+
type TurnStage,
|
|
23
|
+
type TurnIncidentSink,
|
|
24
|
+
type TurnIncidentEvidence,
|
|
25
|
+
} from '../../../dist/agent/turn-trace.js'
|
|
26
|
+
|
|
16
27
|
export const TELEGRAM_MAX_CHARS = 4096
|
|
17
28
|
|
|
18
29
|
/**
|
|
@@ -145,6 +156,10 @@ export const ERROR_MESSAGES: Record<string, string> = {
|
|
|
145
156
|
PROCESS_FAILED: '❌ Claude stopped unexpectedly. Please try sending a new message.',
|
|
146
157
|
POOL_FULL: '⚠️ Too many concurrent sessions. Please try again in a moment.',
|
|
147
158
|
SPAWN_FAILED: '❌ Failed to start Claude session. Please try again.',
|
|
159
|
+
// Epic #195, Phase 3: the interactive backend failed repeatedly, so the agent
|
|
160
|
+
// temporarily fell back to the headless backend to keep serving.
|
|
161
|
+
SAFE_MODE_ENABLED:
|
|
162
|
+
'⚠️ The interactive backend kept failing, so I switched to safe mode (headless) for now. Please resend your message — it should go through.',
|
|
148
163
|
}
|
|
149
164
|
|
|
150
165
|
export const STATUS_EMOJI: Record<string, string> = {
|
|
@@ -177,6 +192,10 @@ export interface WorkingState {
|
|
|
177
192
|
currentReaction: string | null
|
|
178
193
|
lastDetail: string | null
|
|
179
194
|
recentDetails: string[]
|
|
195
|
+
/** Last stage an incident was raised for — dedupes the turn-trace watchdog
|
|
196
|
+
* so one contiguous stalled episode emits a single incident, not one per
|
|
197
|
+
* 15s tick. Reset to null once the turn is no longer stalled. */
|
|
198
|
+
lastIncidentStage: TurnStage | null
|
|
180
199
|
}
|
|
181
200
|
|
|
182
201
|
export interface BotApi {
|
|
@@ -295,6 +314,7 @@ export function createWorkingStateManager(
|
|
|
295
314
|
typingDir: string,
|
|
296
315
|
botApi: BotApi,
|
|
297
316
|
fsApi: FsApi,
|
|
317
|
+
onIncident?: TurnIncidentSink,
|
|
298
318
|
) {
|
|
299
319
|
const states = new Map<string, WorkingState>()
|
|
300
320
|
|
|
@@ -330,6 +350,106 @@ export function createWorkingStateManager(
|
|
|
330
350
|
return `${typingDir}/${chatId}.replied`
|
|
331
351
|
}
|
|
332
352
|
|
|
353
|
+
function menuFilePath(chatId: string): string {
|
|
354
|
+
return `${typingDir}/${chatId}.menu`
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ─── Turn-trace watchdog (Epic #195, Phase 1) ──────────────────────────────
|
|
358
|
+
// Read-only: builds an observation of this turn's on-disk artifacts, classifies
|
|
359
|
+
// the current pipeline stage, and emits a (silent) incident when a stage stalls.
|
|
360
|
+
// It never mutates state or messages the user — the existing heartbeat warn/stop
|
|
361
|
+
// below remains the sole user-facing stalled behaviour.
|
|
362
|
+
|
|
363
|
+
function statMtime(path: string): number | null {
|
|
364
|
+
try {
|
|
365
|
+
return fsApi.existsSync(path) ? fsApi.statSync(path).mtimeMs : null
|
|
366
|
+
} catch {
|
|
367
|
+
return null
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function readTurnObservation(chatId: string, startedAt: number): TurnObservation {
|
|
372
|
+
let statusLabel: string | null = null
|
|
373
|
+
const statusPath = statusFilePath(chatId)
|
|
374
|
+
if (fsApi.existsSync(statusPath)) {
|
|
375
|
+
try {
|
|
376
|
+
statusLabel = parseStatusFile(fsApi.readFileSync(statusPath, 'utf8')).status
|
|
377
|
+
} catch {}
|
|
378
|
+
}
|
|
379
|
+
return {
|
|
380
|
+
now: Date.now(),
|
|
381
|
+
// A live WorkingState means the signal file was written at startedAt.
|
|
382
|
+
signalAt: fsApi.existsSync(typingFilePath(chatId)) ? startedAt : null,
|
|
383
|
+
statusLabel,
|
|
384
|
+
heartbeatAt: statMtime(heartbeatFilePath(chatId)),
|
|
385
|
+
processingAt: statMtime(processingFilePath(chatId)),
|
|
386
|
+
forwardAt: statMtime(forwardFilePath(chatId)),
|
|
387
|
+
menuAt: statMtime(menuFilePath(chatId)),
|
|
388
|
+
repliedPresent: fsApi.existsSync(repliedFilePath(chatId)),
|
|
389
|
+
errorPresent: fsApi.existsSync(errorFilePath(chatId)),
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function readFileSafe(path: string): string | null {
|
|
394
|
+
try {
|
|
395
|
+
return fsApi.existsSync(path) ? fsApi.readFileSync(path, 'utf8') : null
|
|
396
|
+
} catch {
|
|
397
|
+
return null
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Build the diagnostic evidence bundle for an incident from the same
|
|
402
|
+
// observation the classifier used, plus the raw status/error file contents.
|
|
403
|
+
// Cheap and read-only — no listing of the whole typing dir, just this turn's
|
|
404
|
+
// known artifacts. The store scrubs everything before it is persisted.
|
|
405
|
+
function readTurnEvidence(chatId: string, obs: TurnObservation): TurnIncidentEvidence {
|
|
406
|
+
const artifacts: string[] = []
|
|
407
|
+
if (obs.signalAt !== null) artifacts.push('signal')
|
|
408
|
+
if (obs.statusLabel !== null) artifacts.push(`status=${obs.statusLabel}`)
|
|
409
|
+
if (obs.heartbeatAt !== null) artifacts.push('heartbeat')
|
|
410
|
+
if (obs.processingAt !== null) artifacts.push('processing')
|
|
411
|
+
if (obs.forwardAt !== null) artifacts.push('forward')
|
|
412
|
+
if (obs.menuAt !== null) artifacts.push('menu')
|
|
413
|
+
if (obs.repliedPresent) artifacts.push('replied')
|
|
414
|
+
if (obs.errorPresent) artifacts.push('error')
|
|
415
|
+
return {
|
|
416
|
+
artifacts,
|
|
417
|
+
statusText: readFileSafe(statusFilePath(chatId)),
|
|
418
|
+
errorText: readFileSafe(errorFilePath(chatId)),
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function checkTurnTrace(chatId: string, startedAt: number): void {
|
|
423
|
+
const state = states.get(chatId)
|
|
424
|
+
if (!state) return
|
|
425
|
+
const obs = readTurnObservation(chatId, startedAt)
|
|
426
|
+
const trace = classifyTurn(obs)
|
|
427
|
+
if (!trace.stalled) {
|
|
428
|
+
state.lastIncidentStage = null
|
|
429
|
+
return
|
|
430
|
+
}
|
|
431
|
+
// Dedupe: one incident per contiguous stalled-stage episode.
|
|
432
|
+
if (state.lastIncidentStage === trace.stage) return
|
|
433
|
+
state.lastIncidentStage = trace.stage
|
|
434
|
+
if (onIncident) {
|
|
435
|
+
// A fresh .processing sentinel (mtime >= startedAt) means the turn is
|
|
436
|
+
// genuinely mid-work (e.g. a long sub-agent), not silently wedged.
|
|
437
|
+
const midTurn = obs.processingAt !== null && obs.processingAt >= startedAt
|
|
438
|
+
onIncident(
|
|
439
|
+
{
|
|
440
|
+
chatId,
|
|
441
|
+
stage: trace.stage,
|
|
442
|
+
failureClass: trace.failureClass,
|
|
443
|
+
sinceMs: trace.sinceMs,
|
|
444
|
+
budgetMs: trace.budgetMs,
|
|
445
|
+
midTurn,
|
|
446
|
+
at: Date.now(),
|
|
447
|
+
},
|
|
448
|
+
readTurnEvidence(chatId, obs),
|
|
449
|
+
)
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
333
453
|
async function stop(chatId: string): Promise<void> {
|
|
334
454
|
const state = states.get(chatId)
|
|
335
455
|
if (!state) return
|
|
@@ -430,6 +550,7 @@ export function createWorkingStateManager(
|
|
|
430
550
|
currentReaction: null,
|
|
431
551
|
lastDetail: null,
|
|
432
552
|
recentDetails: [],
|
|
553
|
+
lastIncidentStage: null,
|
|
433
554
|
}
|
|
434
555
|
states.set(chatId, state)
|
|
435
556
|
|
|
@@ -530,6 +651,9 @@ export function createWorkingStateManager(
|
|
|
530
651
|
// Heartbeat file is written by SessionProcess on every Claude stdout line.
|
|
531
652
|
state.stalledInterval = setInterval(async () => {
|
|
532
653
|
if (!states.has(chatId)) return
|
|
654
|
+
// Turn-trace watchdog: staged classification + incident telemetry. Silent
|
|
655
|
+
// and side-effect-free — the heartbeat warn/stop below is unchanged.
|
|
656
|
+
checkTurnTrace(chatId, startedAt)
|
|
533
657
|
const hbPath = heartbeatFilePath(chatId)
|
|
534
658
|
let lastActivity = startedAt
|
|
535
659
|
if (fsApi.existsSync(hbPath)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@0xmaxma/claude-gateway",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.31",
|
|
4
4
|
"description": "Multi-agent gateway for Claude",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -28,7 +28,9 @@
|
|
|
28
28
|
"build": "tsc",
|
|
29
29
|
"start": "node --no-warnings=ExperimentalWarning --env-file-if-exists=.env dist/index.js",
|
|
30
30
|
"postinstall": "if command -v bun >/dev/null 2>&1; then bun install --cwd mcp; else echo 'Warning: bun not found. Run `cd mcp && bun install` manually to enable MCP tools.'; fi",
|
|
31
|
+
"pretest": "npm run build",
|
|
31
32
|
"test": "jest",
|
|
33
|
+
"pretest:unit": "npm run build",
|
|
32
34
|
"test:unit": "jest --testPathPattern=unit",
|
|
33
35
|
"test:e2e": "jest --testPathPattern=tests/e2e --forceExit",
|
|
34
36
|
"integration": "jest --testPathPattern=integration --testTimeout=15000",
|