discord-agent-bridge 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +195 -0
- package/dist/app.js +210 -0
- package/dist/app.js.map +1 -0
- package/dist/app.test.js +117 -0
- package/dist/app.test.js.map +1 -0
- package/dist/cli.js +63 -0
- package/dist/cli.js.map +1 -0
- package/dist/cli.test.js +91 -0
- package/dist/cli.test.js.map +1 -0
- package/dist/core/auditLog.js +65 -0
- package/dist/core/auditLog.js.map +1 -0
- package/dist/core/auditLog.test.js +124 -0
- package/dist/core/auditLog.test.js.map +1 -0
- package/dist/core/auth.js +106 -0
- package/dist/core/auth.js.map +1 -0
- package/dist/core/auth.test.js +216 -0
- package/dist/core/auth.test.js.map +1 -0
- package/dist/core/channelRegistry.js +119 -0
- package/dist/core/channelRegistry.js.map +1 -0
- package/dist/core/channelRegistry.test.js +114 -0
- package/dist/core/channelRegistry.test.js.map +1 -0
- package/dist/core/commandPolicy.js +6 -0
- package/dist/core/commandPolicy.js.map +1 -0
- package/dist/core/commandRouter.js +9 -0
- package/dist/core/commandRouter.js.map +1 -0
- package/dist/core/config.js +141 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/config.test.js +149 -0
- package/dist/core/config.test.js.map +1 -0
- package/dist/core/configResolver.js +90 -0
- package/dist/core/configResolver.js.map +1 -0
- package/dist/core/configResolver.test.js +138 -0
- package/dist/core/configResolver.test.js.map +1 -0
- package/dist/core/configSchema.js +164 -0
- package/dist/core/configSchema.js.map +1 -0
- package/dist/core/contracts.js +5 -0
- package/dist/core/contracts.js.map +1 -0
- package/dist/core/eventBus.js +42 -0
- package/dist/core/eventBus.js.map +1 -0
- package/dist/core/eventBus.test.js +72 -0
- package/dist/core/eventBus.test.js.map +1 -0
- package/dist/core/hookBridge.js +11 -0
- package/dist/core/hookBridge.js.map +1 -0
- package/dist/core/logger.js +116 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/logger.test.js +88 -0
- package/dist/core/logger.test.js.map +1 -0
- package/dist/core/modeRegistry.js +33 -0
- package/dist/core/modeRegistry.js.map +1 -0
- package/dist/core/permissionResolver.js +43 -0
- package/dist/core/permissionResolver.js.map +1 -0
- package/dist/core/permissionResolver.test.js +136 -0
- package/dist/core/permissionResolver.test.js.map +1 -0
- package/dist/core/providerCatalog.js +240 -0
- package/dist/core/providerCatalog.js.map +1 -0
- package/dist/core/providerCatalog.test.js +230 -0
- package/dist/core/providerCatalog.test.js.map +1 -0
- package/dist/core/sessionOrchestrator.js +463 -0
- package/dist/core/sessionOrchestrator.js.map +1 -0
- package/dist/core/sessionOrchestrator.test.js +344 -0
- package/dist/core/sessionOrchestrator.test.js.map +1 -0
- package/dist/core/state/schema.js +49 -0
- package/dist/core/state/schema.js.map +1 -0
- package/dist/core/state/store.js +86 -0
- package/dist/core/state/store.js.map +1 -0
- package/dist/core/state/store.test.js +99 -0
- package/dist/core/state/store.test.js.map +1 -0
- package/dist/core/usageService.js +310 -0
- package/dist/core/usageService.js.map +1 -0
- package/dist/core/usageService.test.js +234 -0
- package/dist/core/usageService.test.js.map +1 -0
- package/dist/discord/alwaysAllow.test.js +181 -0
- package/dist/discord/alwaysAllow.test.js.map +1 -0
- package/dist/discord/client.js +666 -0
- package/dist/discord/client.js.map +1 -0
- package/dist/discord/client.test.js +256 -0
- package/dist/discord/client.test.js.map +1 -0
- package/dist/discord/configPanel.js +270 -0
- package/dist/discord/configPanel.js.map +1 -0
- package/dist/discord/configPanel.test.js +332 -0
- package/dist/discord/configPanel.test.js.map +1 -0
- package/dist/discord/directoryBrowser.js +151 -0
- package/dist/discord/directoryBrowser.js.map +1 -0
- package/dist/discord/directoryBrowser.test.js +163 -0
- package/dist/discord/directoryBrowser.test.js.map +1 -0
- package/dist/discord/favorites.js +10 -0
- package/dist/discord/favorites.js.map +1 -0
- package/dist/discord/fileDownload.js +94 -0
- package/dist/discord/fileDownload.js.map +1 -0
- package/dist/discord/fileDownload.test.js +59 -0
- package/dist/discord/fileDownload.test.js.map +1 -0
- package/dist/discord/format.js +104 -0
- package/dist/discord/format.js.map +1 -0
- package/dist/discord/format.test.js +40 -0
- package/dist/discord/format.test.js.map +1 -0
- package/dist/discord/guildChannels.js +97 -0
- package/dist/discord/guildChannels.js.map +1 -0
- package/dist/discord/guildChannels.test.js +231 -0
- package/dist/discord/guildChannels.test.js.map +1 -0
- package/dist/discord/i18n.js +207 -0
- package/dist/discord/i18n.js.map +1 -0
- package/dist/discord/i18n.test.js +31 -0
- package/dist/discord/i18n.test.js.map +1 -0
- package/dist/discord/interactionRouter.js +954 -0
- package/dist/discord/interactionRouter.js.map +1 -0
- package/dist/discord/interactionRouter.test.js +1245 -0
- package/dist/discord/interactionRouter.test.js.map +1 -0
- package/dist/discord/messageRouter.js +221 -0
- package/dist/discord/messageRouter.js.map +1 -0
- package/dist/discord/messageRouter.test.js +360 -0
- package/dist/discord/messageRouter.test.js.map +1 -0
- package/dist/discord/ports.js +12 -0
- package/dist/discord/ports.js.map +1 -0
- package/dist/discord/renderers/defaultSet.test.js +97 -0
- package/dist/discord/renderers/defaultSet.test.js.map +1 -0
- package/dist/discord/renderers/diffView.js +73 -0
- package/dist/discord/renderers/diffView.js.map +1 -0
- package/dist/discord/renderers/diffView.test.js +54 -0
- package/dist/discord/renderers/diffView.test.js.map +1 -0
- package/dist/discord/renderers/index.js +143 -0
- package/dist/discord/renderers/index.js.map +1 -0
- package/dist/discord/renderers/index.test.js +124 -0
- package/dist/discord/renderers/index.test.js.map +1 -0
- package/dist/discord/renderers/mentionOnComplete.js +19 -0
- package/dist/discord/renderers/mentionOnComplete.js.map +1 -0
- package/dist/discord/renderers/mentionOnComplete.test.js +35 -0
- package/dist/discord/renderers/mentionOnComplete.test.js.map +1 -0
- package/dist/discord/renderers/permissionButtons.js +150 -0
- package/dist/discord/renderers/permissionButtons.js.map +1 -0
- package/dist/discord/renderers/permissionButtons.test.js +108 -0
- package/dist/discord/renderers/permissionButtons.test.js.map +1 -0
- package/dist/discord/renderers/resultLine.js +25 -0
- package/dist/discord/renderers/resultLine.js.map +1 -0
- package/dist/discord/renderers/statusEmbed.js +20 -0
- package/dist/discord/renderers/statusEmbed.js.map +1 -0
- package/dist/discord/renderers/statusEmbed.test.js +32 -0
- package/dist/discord/renderers/statusEmbed.test.js.map +1 -0
- package/dist/discord/renderers/streamEmbed.js +134 -0
- package/dist/discord/renderers/streamEmbed.js.map +1 -0
- package/dist/discord/renderers/streamEmbed.test.js +97 -0
- package/dist/discord/renderers/streamEmbed.test.js.map +1 -0
- package/dist/discord/renderers/toolThread.js +65 -0
- package/dist/discord/renderers/toolThread.js.map +1 -0
- package/dist/discord/renderers/toolThread.test.js +54 -0
- package/dist/discord/renderers/toolThread.test.js.map +1 -0
- package/dist/discord/renderers/transcriptFeed.js +33 -0
- package/dist/discord/renderers/transcriptFeed.js.map +1 -0
- package/dist/discord/renderers/usageEmbed.js +78 -0
- package/dist/discord/renderers/usageEmbed.js.map +1 -0
- package/dist/discord/renderers/usageEmbed.test.js +42 -0
- package/dist/discord/renderers/usageEmbed.test.js.map +1 -0
- package/dist/discord/wiring.js +214 -0
- package/dist/discord/wiring.js.map +1 -0
- package/dist/discord/wiring.test.js +252 -0
- package/dist/discord/wiring.test.js.map +1 -0
- package/dist/discord/wizard/channelWizard.js +305 -0
- package/dist/discord/wizard/channelWizard.js.map +1 -0
- package/dist/discord/wizard/channelWizard.test.js +298 -0
- package/dist/discord/wizard/channelWizard.test.js.map +1 -0
- package/dist/discord/wizard/resumeWizard.js +123 -0
- package/dist/discord/wizard/resumeWizard.js.map +1 -0
- package/dist/modes/claude/index.js +80 -0
- package/dist/modes/claude/index.js.map +1 -0
- package/dist/modes/claude/listResumable.test.js +72 -0
- package/dist/modes/claude/listResumable.test.js.map +1 -0
- package/dist/modes/claude/mcpFileTool.js +82 -0
- package/dist/modes/claude/mcpFileTool.js.map +1 -0
- package/dist/modes/claude/permissions.js +35 -0
- package/dist/modes/claude/permissions.js.map +1 -0
- package/dist/modes/claude/plugins.js +56 -0
- package/dist/modes/claude/plugins.js.map +1 -0
- package/dist/modes/claude/session.js +324 -0
- package/dist/modes/claude/session.js.map +1 -0
- package/dist/modes/claude/session.test.js +415 -0
- package/dist/modes/claude/session.test.js.map +1 -0
- package/dist/modes/codex/discovery.js +221 -0
- package/dist/modes/codex/discovery.js.map +1 -0
- package/dist/modes/codex/discovery.test.js +213 -0
- package/dist/modes/codex/discovery.test.js.map +1 -0
- package/dist/modes/codex/eventMapper.js +262 -0
- package/dist/modes/codex/eventMapper.js.map +1 -0
- package/dist/modes/codex/index.js +132 -0
- package/dist/modes/codex/index.js.map +1 -0
- package/dist/modes/codex/index.test.js +170 -0
- package/dist/modes/codex/index.test.js.map +1 -0
- package/dist/modes/codex/runner.js +207 -0
- package/dist/modes/codex/runner.js.map +1 -0
- package/dist/modes/codex/runner.test.js +587 -0
- package/dist/modes/codex/runner.test.js.map +1 -0
- package/dist/modes/codex/sqliteReader.js +171 -0
- package/dist/modes/codex/sqliteReader.js.map +1 -0
- package/dist/modes/codex/sqliteReader.test.js +137 -0
- package/dist/modes/codex/sqliteReader.test.js.map +1 -0
- package/dist/setup/wizard.js +115 -0
- package/dist/setup/wizard.js.map +1 -0
- package/dist/setup/wizard.test.js +216 -0
- package/dist/setup/wizard.test.js.map +1 -0
- package/dist/startBot.test.js +78 -0
- package/dist/startBot.test.js.map +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { COLORS } from '../format.js';
|
|
2
|
+
import { t } from '../i18n.js';
|
|
3
|
+
// Usage/limits panel (§6, §7.4 — Claude, cap usagePanel): renders a UsageResult
|
|
4
|
+
// (5-hour + weekly + per-model) plus the context % from the latest context_usage
|
|
5
|
+
// event. When usage is unavailable (API-key-only user, or the Codex "unsupported"
|
|
6
|
+
// result), the panel renders NOTHING — buildUsageEmbed returns null so 7b simply
|
|
7
|
+
// posts no panel. Ports A4D buildUsageEmbed layout (progress bars + reset time).
|
|
8
|
+
const BAR_LEN = 20;
|
|
9
|
+
const BAR_FILLED = '▓';
|
|
10
|
+
const BAR_EMPTY = '░';
|
|
11
|
+
// Utilization thresholds → embed color (A4D utilizationColor).
|
|
12
|
+
function utilizationColor(maxUtil) {
|
|
13
|
+
if (maxUtil >= 90)
|
|
14
|
+
return COLORS.stopped;
|
|
15
|
+
if (maxUtil >= 70)
|
|
16
|
+
return COLORS.streaming;
|
|
17
|
+
return COLORS.idle;
|
|
18
|
+
}
|
|
19
|
+
function progressBar(utilization) {
|
|
20
|
+
const clamped = Math.max(0, Math.min(100, utilization));
|
|
21
|
+
const filled = Math.round((clamped / 100) * BAR_LEN);
|
|
22
|
+
return BAR_FILLED.repeat(filled) + BAR_EMPTY.repeat(BAR_LEN - filled);
|
|
23
|
+
}
|
|
24
|
+
// A "resets <t:unix:R>" Discord relative-timestamp line, or '' when no reset is known.
|
|
25
|
+
function resetLine(limit) {
|
|
26
|
+
if (!limit.resetsAt)
|
|
27
|
+
return '';
|
|
28
|
+
const ms = Date.parse(limit.resetsAt);
|
|
29
|
+
if (Number.isNaN(ms))
|
|
30
|
+
return '';
|
|
31
|
+
return '\n' + t('usage.resets', { reset: `<t:${Math.floor(ms / 1000)}:R>` });
|
|
32
|
+
}
|
|
33
|
+
function limitField(label, limit, inline) {
|
|
34
|
+
return {
|
|
35
|
+
name: label,
|
|
36
|
+
value: `${progressBar(limit.utilization)} **${Math.round(limit.utilization)}%**${resetLine(limit)}`,
|
|
37
|
+
...(inline ? { inline: true } : {}),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
// Build the usage embed. Returns null when nothing should be shown (unavailable
|
|
41
|
+
// usage AND no context figure). `ctxUsage` is the latest context_usage event or null.
|
|
42
|
+
export function buildUsageEmbed(usage, ctxUsage) {
|
|
43
|
+
const haveUsage = usage !== null && 'fetchedAt' in usage;
|
|
44
|
+
if (!haveUsage && !ctxUsage)
|
|
45
|
+
return null;
|
|
46
|
+
const fields = [];
|
|
47
|
+
let maxUtil = 0;
|
|
48
|
+
if (haveUsage) {
|
|
49
|
+
const snap = usage;
|
|
50
|
+
if (snap.fiveHour) {
|
|
51
|
+
fields.push(limitField(t('usage.fiveHour'), snap.fiveHour));
|
|
52
|
+
maxUtil = Math.max(maxUtil, snap.fiveHour.utilization);
|
|
53
|
+
}
|
|
54
|
+
if (snap.sevenDay) {
|
|
55
|
+
fields.push(limitField(t('usage.weekly'), snap.sevenDay));
|
|
56
|
+
maxUtil = Math.max(maxUtil, snap.sevenDay.utilization);
|
|
57
|
+
}
|
|
58
|
+
if (snap.sevenDayOpus)
|
|
59
|
+
fields.push(limitField(t('usage.weeklyOpus'), snap.sevenDayOpus, true));
|
|
60
|
+
if (snap.sevenDaySonnet)
|
|
61
|
+
fields.push(limitField(t('usage.weeklySonnet'), snap.sevenDaySonnet, true));
|
|
62
|
+
}
|
|
63
|
+
if (ctxUsage) {
|
|
64
|
+
fields.push({
|
|
65
|
+
name: t('usage.context'),
|
|
66
|
+
value: `${progressBar(ctxUsage.percentage)} **${Math.round(ctxUsage.percentage)}%**`,
|
|
67
|
+
});
|
|
68
|
+
maxUtil = Math.max(maxUtil, ctxUsage.percentage);
|
|
69
|
+
}
|
|
70
|
+
if (fields.length === 0)
|
|
71
|
+
return null;
|
|
72
|
+
return {
|
|
73
|
+
title: t('usage.title'),
|
|
74
|
+
color: utilizationColor(maxUtil),
|
|
75
|
+
fields,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=usageEmbed.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usageEmbed.js","sourceRoot":"","sources":["../../../src/discord/renderers/usageEmbed.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { buildUsageEmbed } from './usageEmbed.js';
|
|
3
|
+
const ctx = {
|
|
4
|
+
kind: 'context_usage',
|
|
5
|
+
totalTokens: 30,
|
|
6
|
+
maxTokens: 100,
|
|
7
|
+
percentage: 30,
|
|
8
|
+
};
|
|
9
|
+
describe('buildUsageEmbed', () => {
|
|
10
|
+
it('renders nothing when usage is unavailable and there is no context figure', () => {
|
|
11
|
+
const unavailable = { available: false, reason: 'no-credentials' };
|
|
12
|
+
expect(buildUsageEmbed(unavailable, null)).toBeNull();
|
|
13
|
+
expect(buildUsageEmbed(null, null)).toBeNull();
|
|
14
|
+
});
|
|
15
|
+
it('renders the codex-unsupported result as nothing (hidden panel)', () => {
|
|
16
|
+
const codex = { available: false, reason: 'codex-unsupported' };
|
|
17
|
+
expect(buildUsageEmbed(codex, null)).toBeNull();
|
|
18
|
+
});
|
|
19
|
+
it('renders 5-hour + weekly + per-model + context fields from a snapshot', () => {
|
|
20
|
+
const snapshot = {
|
|
21
|
+
fetchedAt: 1000,
|
|
22
|
+
fiveHour: { utilization: 42, resetsAt: '2026-07-01T12:00:00Z' },
|
|
23
|
+
sevenDay: { utilization: 10 },
|
|
24
|
+
sevenDayOpus: { utilization: 80 },
|
|
25
|
+
sevenDaySonnet: { utilization: 5 },
|
|
26
|
+
};
|
|
27
|
+
const embed = buildUsageEmbed(snapshot, ctx);
|
|
28
|
+
const names = (embed?.fields ?? []).map((f) => f.name);
|
|
29
|
+
expect(names).toContain('5시간');
|
|
30
|
+
expect(names).toContain('주간');
|
|
31
|
+
expect(names).toContain('주간 (Opus)');
|
|
32
|
+
expect(names).toContain('컨텍스트');
|
|
33
|
+
// Highest utilization (80) → yellow band color, not green.
|
|
34
|
+
expect(embed?.color).not.toBeUndefined();
|
|
35
|
+
});
|
|
36
|
+
it('renders a context-only panel when only a context figure is present', () => {
|
|
37
|
+
const embed = buildUsageEmbed({ available: false, reason: 'no-credentials' }, ctx);
|
|
38
|
+
expect(embed).not.toBeNull();
|
|
39
|
+
expect((embed?.fields ?? []).map((f) => f.name)).toEqual(['컨텍스트']);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
//# sourceMappingURL=usageEmbed.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usageEmbed.test.js","sourceRoot":"","sources":["../../../src/discord/renderers/usageEmbed.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAIlD,MAAM,GAAG,GAAmD;IAC1D,IAAI,EAAE,eAAe;IACrB,WAAW,EAAE,EAAE;IACf,SAAS,EAAE,GAAG;IACd,UAAU,EAAE,EAAE;CACf,CAAC;AAEF,QAAQ,CAAC,iBAAiB,EAAE,GAAG,EAAE;IAC/B,EAAE,CAAC,0EAA0E,EAAE,GAAG,EAAE;QAClF,MAAM,WAAW,GAAgB,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC;QAChF,MAAM,CAAC,eAAe,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;QACtD,MAAM,CAAC,eAAe,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;IACjD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gEAAgE,EAAE,GAAG,EAAE;QACxE,MAAM,KAAK,GAAgB,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,mBAAmB,EAAE,CAAC;QAC7E,MAAM,CAAC,eAAe,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;IAClD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sEAAsE,EAAE,GAAG,EAAE;QAC9E,MAAM,QAAQ,GAAgB;YAC5B,SAAS,EAAE,IAAI;YACf,QAAQ,EAAE,EAAE,WAAW,EAAE,EAAE,EAAE,QAAQ,EAAE,sBAAsB,EAAE;YAC/D,QAAQ,EAAE,EAAE,WAAW,EAAE,EAAE,EAAE;YAC7B,YAAY,EAAE,EAAE,WAAW,EAAE,EAAE,EAAE;YACjC,cAAc,EAAE,EAAE,WAAW,EAAE,CAAC,EAAE;SACnC,CAAC;QACF,MAAM,KAAK,GAAG,eAAe,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;QAC7C,MAAM,KAAK,GAAG,CAAC,KAAK,EAAE,MAAM,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QACvD,MAAM,CAAC,KAAK,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QAC/B,MAAM,CAAC,KAAK,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QAC9B,MAAM,CAAC,KAAK,CAAC,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;QACrC,MAAM,CAAC,KAAK,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;QAChC,2DAA2D;QAC3D,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC;IAC3C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oEAAoE,EAAE,GAAG,EAAE;QAC5E,MAAM,KAAK,GAAG,eAAe,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,EAAE,GAAG,CAAC,CAAC;QACnF,MAAM,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;QAC7B,MAAM,CAAC,CAAC,KAAK,EAAE,MAAM,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;IACrE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { codexUsageUnavailable } from '../core/usageService.js';
|
|
2
|
+
import { RendererDispatcher, createDefaultRendererSet } from './renderers/index.js';
|
|
3
|
+
import { PermissionButtonsHandler, parseCustomId } from './renderers/permissionButtons.js';
|
|
4
|
+
import { resolveChannelAdapter } from './client.js';
|
|
5
|
+
function channelKey(guildId, channelId) {
|
|
6
|
+
return `${guildId}:${channelId}`;
|
|
7
|
+
}
|
|
8
|
+
export class SessionWiring {
|
|
9
|
+
eventBus;
|
|
10
|
+
modeRegistry;
|
|
11
|
+
channelRegistry;
|
|
12
|
+
usageService;
|
|
13
|
+
logger;
|
|
14
|
+
// Mutable so app boot can bind it to the live gateway AFTER the client is
|
|
15
|
+
// constructed (the client depends on the routers which depend on this wiring —
|
|
16
|
+
// resolveChannel is only used at attach()/sendFile time, i.e. after login).
|
|
17
|
+
resolveChannel;
|
|
18
|
+
permissionTimeoutMs;
|
|
19
|
+
auditLog;
|
|
20
|
+
onAlwaysAllow;
|
|
21
|
+
channels = new Map();
|
|
22
|
+
// Latest usage snapshot, refreshed lazily; fed into the usage embed synchronously.
|
|
23
|
+
lastUsage = null;
|
|
24
|
+
constructor(deps) {
|
|
25
|
+
this.eventBus = deps.eventBus;
|
|
26
|
+
this.modeRegistry = deps.modeRegistry;
|
|
27
|
+
this.channelRegistry = deps.channelRegistry;
|
|
28
|
+
this.usageService = deps.usageService;
|
|
29
|
+
this.logger = deps.logger;
|
|
30
|
+
this.auditLog = deps.auditLog;
|
|
31
|
+
this.resolveChannel = deps.resolveChannel ?? (() => Promise.resolve(null));
|
|
32
|
+
this.permissionTimeoutMs = Math.max(1, deps.permissionTimeoutSec) * 1000;
|
|
33
|
+
this.onAlwaysAllow = deps.onAlwaysAllow;
|
|
34
|
+
}
|
|
35
|
+
// Build the live resolveChannel over a real gateway client (used by app boot).
|
|
36
|
+
static resolveOverClient(client) {
|
|
37
|
+
return (channelId) => resolveChannelAdapter(client, channelId);
|
|
38
|
+
}
|
|
39
|
+
// Bind (or rebind) the channel resolver. App boot calls this once the gateway
|
|
40
|
+
// client exists to point the wiring at the live client's channel lookup.
|
|
41
|
+
setResolveChannel(resolveChannel) {
|
|
42
|
+
this.resolveChannel = resolveChannel;
|
|
43
|
+
}
|
|
44
|
+
// The orchestrator's requestPermission hook (§7.5): post the buttons on the bound
|
|
45
|
+
// channel's permission handler and await the decision, denying on timeout. If the
|
|
46
|
+
// channel is not wired yet (renderers not attached) fall back to deny — a prompt
|
|
47
|
+
// the operator can never see must never auto-allow.
|
|
48
|
+
requestPermission = async (binding, req) => {
|
|
49
|
+
const wiring = this.channels.get(channelKey(binding.guildId, binding.channelId));
|
|
50
|
+
if (!wiring) {
|
|
51
|
+
return { behavior: 'deny', message: 'No live channel to prompt; denied.' };
|
|
52
|
+
}
|
|
53
|
+
// The reqId is embedded in the button custom_id `perm:<reqId>:<action>`, which
|
|
54
|
+
// parseCustomId splits on ':' — so the id itself MUST NOT contain a colon.
|
|
55
|
+
const id = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
56
|
+
const ev = {
|
|
57
|
+
kind: 'permission_request',
|
|
58
|
+
id,
|
|
59
|
+
toolName: req.toolName,
|
|
60
|
+
input: req.input,
|
|
61
|
+
};
|
|
62
|
+
// Bind the prompt to the session owner (driver): only they may resolve it, so a
|
|
63
|
+
// bystander in the channel cannot approve another driver's tool (§7.1/§7.5).
|
|
64
|
+
const decision = wiring.permission.request(ev, binding.ownerId);
|
|
65
|
+
return this.withTimeout(decision, id, wiring);
|
|
66
|
+
};
|
|
67
|
+
// Resolve a `perm:<reqId>:<action>` button for a channel. `actorId` is the Discord
|
|
68
|
+
// user who clicked; the prompt is bound to the session owner, so an actor other
|
|
69
|
+
// than the owner is ignored (resolve returns null, prompt stays pending). Returns
|
|
70
|
+
// the applied decision, or null when the id is foreign/unknown or the actor is not
|
|
71
|
+
// the approver (safe on any interaction).
|
|
72
|
+
async resolvePermission(guildId, channelId, customId, actorId) {
|
|
73
|
+
const wiring = this.channels.get(channelKey(guildId, channelId));
|
|
74
|
+
if (!wiring)
|
|
75
|
+
return null;
|
|
76
|
+
// When the action is "always-allow", read the tool name BEFORE resolve()
|
|
77
|
+
// removes the pending entry, then persist it into the config auto-allow set so
|
|
78
|
+
// future turns skip the prompt (§7A). resolve() itself still settles the turn's
|
|
79
|
+
// decision as a plain allow (permissionButtons.resolve).
|
|
80
|
+
const parsed = parseCustomId(customId);
|
|
81
|
+
const alwaysTool = parsed?.action === 'always' ? wiring.permission.peekToolName(parsed.reqId) : null;
|
|
82
|
+
const decision = await wiring.permission.resolve(customId, actorId);
|
|
83
|
+
if (alwaysTool && decision?.behavior === 'allow') {
|
|
84
|
+
// Audit the GLOBAL always-allow write (§7.5) around the persistence: record
|
|
85
|
+
// the actor, tool, and channel so the who/when/what of an always-allow is
|
|
86
|
+
// durable even though the config write itself is best-effort. The audit and
|
|
87
|
+
// the persist are independently guarded so neither failure breaks the turn
|
|
88
|
+
// (already allowed) nor the other side effect.
|
|
89
|
+
this.auditAlwaysAllow(actorId ?? '', guildId, channelId, alwaysTool);
|
|
90
|
+
try {
|
|
91
|
+
this.onAlwaysAllow?.(alwaysTool, { actorId: actorId ?? '', guildId, channelId });
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
// Persisting the always-allow set is best-effort; a config write failure
|
|
95
|
+
// must not break the interaction (the turn is already allowed).
|
|
96
|
+
this.logger.warn('failed to persist always-allow tool', { tool: alwaysTool, err: String(err) });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return decision;
|
|
100
|
+
}
|
|
101
|
+
// Record an always-allow grant to the audit trail. Best-effort: AuditLog.record
|
|
102
|
+
// never throws, but the whole call is still guarded so an absent/failing audit
|
|
103
|
+
// sink never breaks the interaction.
|
|
104
|
+
auditAlwaysAllow(actorId, guildId, channelId, tool) {
|
|
105
|
+
if (!this.auditLog)
|
|
106
|
+
return;
|
|
107
|
+
try {
|
|
108
|
+
this.auditLog.record({
|
|
109
|
+
actorId,
|
|
110
|
+
roleTier: 'execute',
|
|
111
|
+
guildId,
|
|
112
|
+
channelId,
|
|
113
|
+
action: 'always-allow',
|
|
114
|
+
tool,
|
|
115
|
+
status: 'allowed',
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
this.logger.warn('failed to audit always-allow', { tool, err: String(err) });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// Attach renderers + permission handler for a channel that just started/resumed.
|
|
123
|
+
// Idempotent: re-attaching first tears down any prior subscription so a resume
|
|
124
|
+
// after a restart does not double-render.
|
|
125
|
+
async attach(guildId, channelId, mode) {
|
|
126
|
+
const key = channelKey(guildId, channelId);
|
|
127
|
+
this.detach(guildId, channelId);
|
|
128
|
+
const channel = await this.resolveChannel(channelId);
|
|
129
|
+
if (!channel) {
|
|
130
|
+
this.logger.warn('cannot wire renderers: channel unresolved', { guildId, channelId });
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const binding = this.channelRegistry.get(guildId, channelId);
|
|
134
|
+
const ownerId = binding?.ownerId ?? '';
|
|
135
|
+
const capabilities = this.modeRegistry.get(mode).capabilities;
|
|
136
|
+
const permission = new PermissionButtonsHandler({ channel });
|
|
137
|
+
const rendererSet = createDefaultRendererSet({
|
|
138
|
+
channel,
|
|
139
|
+
ownerId,
|
|
140
|
+
getUsage: () => this.usageSnapshotFor(mode),
|
|
141
|
+
});
|
|
142
|
+
// The dispatcher's permission renderer posts buttons via its own handler; we
|
|
143
|
+
// want the SAME handler instance the router resolves against, so route the
|
|
144
|
+
// permission event through our shared handler instead of the set's private one.
|
|
145
|
+
const set = { ...rendererSet, permission: (ev) => { void permission.request(ev).catch(() => { }); } };
|
|
146
|
+
const dispatcher = new RendererDispatcher(set, capabilities);
|
|
147
|
+
const unsubscribe = dispatcher.subscribe(this.eventBus, guildId, channelId);
|
|
148
|
+
this.channels.set(key, { unsubscribe, renderers: set, permission, channel, mode });
|
|
149
|
+
// Kick a usage refresh so the first usage embed has data (Claude only).
|
|
150
|
+
if (capabilities.usagePanel)
|
|
151
|
+
void this.refreshUsage();
|
|
152
|
+
}
|
|
153
|
+
// Tear down a channel's renderer subscription on stop/close. Unsubscribe first so
|
|
154
|
+
// no new event can arm a renderer, THEN dispose the renderer set so any already-
|
|
155
|
+
// armed stream/thinking debounce timer is cancelled — a /stop mid-stream must not
|
|
156
|
+
// fire a late send/edit or orphan a "Responding…" embed (§6).
|
|
157
|
+
detach(guildId, channelId) {
|
|
158
|
+
const key = channelKey(guildId, channelId);
|
|
159
|
+
const wiring = this.channels.get(key);
|
|
160
|
+
if (!wiring)
|
|
161
|
+
return;
|
|
162
|
+
wiring.unsubscribe();
|
|
163
|
+
try {
|
|
164
|
+
wiring.renderers.dispose?.();
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
this.logger.warn('renderer dispose failed on detach', { guildId, channelId, err: String(err) });
|
|
168
|
+
}
|
|
169
|
+
this.channels.delete(key);
|
|
170
|
+
}
|
|
171
|
+
// The sendFile callback for a channel's mcpFileTool: post the confined file to the
|
|
172
|
+
// bound channel. Bound per channel at start time.
|
|
173
|
+
sendFileFor(guildId, channelId) {
|
|
174
|
+
return async (absPath, filename) => {
|
|
175
|
+
const wiring = this.channels.get(channelKey(guildId, channelId));
|
|
176
|
+
if (!wiring)
|
|
177
|
+
throw new Error('Channel is not wired; cannot send file.');
|
|
178
|
+
await wiring.channel.send({ files: [{ path: absPath, ...(filename ? { name: filename } : {}) }] });
|
|
179
|
+
return `Sent ${filename ?? absPath} to the channel.`;
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
// Refresh the cached usage snapshot (Claude OAuth usage). Best-effort; never throws.
|
|
183
|
+
async refreshUsage() {
|
|
184
|
+
try {
|
|
185
|
+
this.lastUsage = await this.usageService.getUsage();
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
this.logger.warn('usage refresh failed', { err: String(err) });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// Usage feed for the embed: Codex is structurally unavailable; Claude uses the
|
|
192
|
+
// last poll snapshot (may be null until the first refresh completes).
|
|
193
|
+
usageSnapshotFor(mode) {
|
|
194
|
+
if (this.modeRegistry.has(mode) && !this.modeRegistry.get(mode).capabilities.usagePanel) {
|
|
195
|
+
return codexUsageUnavailable();
|
|
196
|
+
}
|
|
197
|
+
return this.lastUsage;
|
|
198
|
+
}
|
|
199
|
+
// Race the decision against the permission timeout. On timeout, resolve the
|
|
200
|
+
// pending prompt as deny (via a synthetic deny custom_id) and return deny.
|
|
201
|
+
withTimeout(decision, reqId, wiring) {
|
|
202
|
+
return new Promise((resolve) => {
|
|
203
|
+
const timer = setTimeout(() => {
|
|
204
|
+
void wiring.permission.resolve(`perm:${reqId}:deny`).catch(() => { });
|
|
205
|
+
resolve({ behavior: 'deny', message: 'Permission request timed out; denied.' });
|
|
206
|
+
}, this.permissionTimeoutMs);
|
|
207
|
+
void decision.then((d) => {
|
|
208
|
+
clearTimeout(timer);
|
|
209
|
+
resolve(d);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
//# sourceMappingURL=wiring.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wiring.js","sourceRoot":"","sources":["../../src/discord/wiring.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { SessionWiring } from './wiring.js';
|
|
3
|
+
import { EventBus } from '../core/eventBus.js';
|
|
4
|
+
import { ModeRegistry } from '../core/modeRegistry.js';
|
|
5
|
+
import { createLogger } from '../core/logger.js';
|
|
6
|
+
import { parseCustomId } from './renderers/permissionButtons.js';
|
|
7
|
+
const logger = createLogger('test', { level: 'error', sink: { write() { } } });
|
|
8
|
+
const CLAUDE_CAPS = {
|
|
9
|
+
streaming: true,
|
|
10
|
+
thinking: true,
|
|
11
|
+
toolThreads: true,
|
|
12
|
+
permissionPrompts: true,
|
|
13
|
+
progress: false,
|
|
14
|
+
transcript: false,
|
|
15
|
+
sessionResume: true,
|
|
16
|
+
fileAttach: true,
|
|
17
|
+
fileDiff: true,
|
|
18
|
+
usagePanel: true,
|
|
19
|
+
permissionModes: ['default', 'acceptEdits', 'bypassPermissions', 'plan'],
|
|
20
|
+
};
|
|
21
|
+
class StubMode {
|
|
22
|
+
name;
|
|
23
|
+
capabilities;
|
|
24
|
+
constructor(name, capabilities) {
|
|
25
|
+
this.name = name;
|
|
26
|
+
this.capabilities = capabilities;
|
|
27
|
+
}
|
|
28
|
+
async start(_ctx) {
|
|
29
|
+
return { sessionId: 's', async send() { }, async stop() { } };
|
|
30
|
+
}
|
|
31
|
+
async resume(_ctx, id) {
|
|
32
|
+
return { sessionId: id, async send() { }, async stop() { } };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// A fake MessageChannel port that records every sent message and hands back an
|
|
36
|
+
// editable handle. The permission handler posts its buttons through this.
|
|
37
|
+
function fakeChannel() {
|
|
38
|
+
const sent = [];
|
|
39
|
+
const channel = {
|
|
40
|
+
send: async (message) => {
|
|
41
|
+
sent.push(message);
|
|
42
|
+
return { id: `m${sent.length}`, async edit() { } };
|
|
43
|
+
},
|
|
44
|
+
startThread: async () => {
|
|
45
|
+
throw new Error('not used');
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
return { channel, sent };
|
|
49
|
+
}
|
|
50
|
+
function makeWiring(opts) {
|
|
51
|
+
const eventBus = new EventBus();
|
|
52
|
+
const modeRegistry = new ModeRegistry();
|
|
53
|
+
modeRegistry.register(new StubMode('claude', CLAUDE_CAPS));
|
|
54
|
+
modeRegistry.register(new StubMode('codex', { ...CLAUDE_CAPS, usagePanel: false }));
|
|
55
|
+
const channelRegistry = {
|
|
56
|
+
get: () => ({ ownerId: opts.ownerId ?? 'owner' }),
|
|
57
|
+
};
|
|
58
|
+
const usageService = {
|
|
59
|
+
isAvailable: () => opts.usage !== undefined,
|
|
60
|
+
getUsage: async () => opts.usage ?? { available: false, reason: 'no-credentials' },
|
|
61
|
+
};
|
|
62
|
+
const wiring = new SessionWiring({
|
|
63
|
+
eventBus,
|
|
64
|
+
modeRegistry,
|
|
65
|
+
channelRegistry,
|
|
66
|
+
usageService,
|
|
67
|
+
logger,
|
|
68
|
+
resolveChannel: async () => opts.channel,
|
|
69
|
+
permissionTimeoutSec: opts.permissionTimeoutSec ?? 60,
|
|
70
|
+
...(opts.auditLog ? { auditLog: opts.auditLog } : {}),
|
|
71
|
+
...(opts.onAlwaysAllow ? { onAlwaysAllow: opts.onAlwaysAllow } : {}),
|
|
72
|
+
});
|
|
73
|
+
return { wiring, eventBus };
|
|
74
|
+
}
|
|
75
|
+
// A fake AuditLog that captures recorded entries in memory (no fs). Only record()
|
|
76
|
+
// is exercised by the wiring, so the rest is left unimplemented.
|
|
77
|
+
function fakeAuditLog() {
|
|
78
|
+
const records = [];
|
|
79
|
+
const auditLog = {
|
|
80
|
+
record: (entry) => {
|
|
81
|
+
const rec = { ...entry, timestamp: 't0' };
|
|
82
|
+
records.push(rec);
|
|
83
|
+
return rec;
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
return { auditLog, records };
|
|
87
|
+
}
|
|
88
|
+
// Extract the perm request id from the buttons the handler posted (customId
|
|
89
|
+
// perm:<reqId>:allow). Returns the reqId or null.
|
|
90
|
+
function reqIdFromSent(sent) {
|
|
91
|
+
for (const m of sent) {
|
|
92
|
+
for (const row of m.components ?? []) {
|
|
93
|
+
for (const c of row.components) {
|
|
94
|
+
if (c.type === 'button') {
|
|
95
|
+
const parsed = parseCustomId(c.customId);
|
|
96
|
+
if (parsed)
|
|
97
|
+
return parsed.reqId;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
const binding = { guildId: 'g1', channelId: 'c1', ownerId: 'owner' };
|
|
105
|
+
describe('SessionWiring.requestPermission', () => {
|
|
106
|
+
it('resolves via a simulated Allow button', async () => {
|
|
107
|
+
const { channel, sent } = fakeChannel();
|
|
108
|
+
const { wiring } = makeWiring({ channel });
|
|
109
|
+
await wiring.attach('g1', 'c1', 'claude');
|
|
110
|
+
const decisionP = wiring.requestPermission(binding, { toolName: 'Bash', input: { command: 'ls' } });
|
|
111
|
+
// Let the buttons post (request() posts asynchronously).
|
|
112
|
+
await Promise.resolve();
|
|
113
|
+
await Promise.resolve();
|
|
114
|
+
const reqId = reqIdFromSent(sent);
|
|
115
|
+
expect(reqId).not.toBeNull();
|
|
116
|
+
// Simulate the user clicking Allow.
|
|
117
|
+
const resolved = await wiring.resolvePermission('g1', 'c1', `perm:${reqId}:allow`);
|
|
118
|
+
expect(resolved).toEqual({ behavior: 'allow' });
|
|
119
|
+
const decision = await decisionP;
|
|
120
|
+
expect(decision.behavior).toBe('allow');
|
|
121
|
+
});
|
|
122
|
+
it('a resolve from a NON-owner actor is ignored; the owner resolves it', async () => {
|
|
123
|
+
const { channel, sent } = fakeChannel();
|
|
124
|
+
// The prompt is bound to the session owner via binding.ownerId.
|
|
125
|
+
const { wiring } = makeWiring({ channel, ownerId: 'owner' });
|
|
126
|
+
await wiring.attach('g1', 'c1', 'claude');
|
|
127
|
+
const decisionP = wiring.requestPermission(binding, { toolName: 'Bash', input: { command: 'rm' } });
|
|
128
|
+
await Promise.resolve();
|
|
129
|
+
await Promise.resolve();
|
|
130
|
+
const reqId = reqIdFromSent(sent);
|
|
131
|
+
// A bystander (different execute-tier user) clicks Allow → ignored, stays pending.
|
|
132
|
+
const bystander = await wiring.resolvePermission('g1', 'c1', `perm:${reqId}:allow`, 'bystander');
|
|
133
|
+
expect(bystander).toBeNull();
|
|
134
|
+
// The owner clicks Allow → now it resolves.
|
|
135
|
+
const owner = await wiring.resolvePermission('g1', 'c1', `perm:${reqId}:allow`, 'owner');
|
|
136
|
+
expect(owner).toEqual({ behavior: 'allow' });
|
|
137
|
+
const decision = await decisionP;
|
|
138
|
+
expect(decision.behavior).toBe('allow');
|
|
139
|
+
});
|
|
140
|
+
it('always-allow records an audit entry with the actor id before persisting', async () => {
|
|
141
|
+
const { channel, sent } = fakeChannel();
|
|
142
|
+
const { auditLog, records } = fakeAuditLog();
|
|
143
|
+
const persisted = [];
|
|
144
|
+
const { wiring } = makeWiring({
|
|
145
|
+
channel,
|
|
146
|
+
ownerId: 'owner',
|
|
147
|
+
auditLog,
|
|
148
|
+
onAlwaysAllow: (tool, ctx) => persisted.push({ tool, actorId: ctx.actorId }),
|
|
149
|
+
});
|
|
150
|
+
await wiring.attach('g1', 'c1', 'claude');
|
|
151
|
+
const decisionP = wiring.requestPermission(binding, { toolName: 'Bash', input: {} });
|
|
152
|
+
await Promise.resolve();
|
|
153
|
+
await Promise.resolve();
|
|
154
|
+
const reqId = reqIdFromSent(sent);
|
|
155
|
+
await wiring.resolvePermission('g1', 'c1', `perm:${reqId}:always`, 'owner');
|
|
156
|
+
await decisionP;
|
|
157
|
+
// The GLOBAL always-allow write is audited with the actor + tool + channel.
|
|
158
|
+
const entry = records.find((r) => r.action === 'always-allow');
|
|
159
|
+
expect(entry).toBeDefined();
|
|
160
|
+
expect(entry).toMatchObject({
|
|
161
|
+
actorId: 'owner',
|
|
162
|
+
action: 'always-allow',
|
|
163
|
+
tool: 'Bash',
|
|
164
|
+
guildId: 'g1',
|
|
165
|
+
channelId: 'c1',
|
|
166
|
+
});
|
|
167
|
+
// And the persistence still ran with the same actor.
|
|
168
|
+
expect(persisted).toEqual([{ tool: 'Bash', actorId: 'owner' }]);
|
|
169
|
+
});
|
|
170
|
+
it('a simulated Deny button resolves to deny', async () => {
|
|
171
|
+
const { channel, sent } = fakeChannel();
|
|
172
|
+
const { wiring } = makeWiring({ channel });
|
|
173
|
+
await wiring.attach('g1', 'c1', 'claude');
|
|
174
|
+
const decisionP = wiring.requestPermission(binding, { toolName: 'Bash', input: {} });
|
|
175
|
+
await Promise.resolve();
|
|
176
|
+
await Promise.resolve();
|
|
177
|
+
const reqId = reqIdFromSent(sent);
|
|
178
|
+
await wiring.resolvePermission('g1', 'c1', `perm:${reqId}:deny`);
|
|
179
|
+
const decision = await decisionP;
|
|
180
|
+
expect(decision.behavior).toBe('deny');
|
|
181
|
+
});
|
|
182
|
+
it('times out → deny (no button click)', async () => {
|
|
183
|
+
vi.useFakeTimers();
|
|
184
|
+
try {
|
|
185
|
+
const { channel } = fakeChannel();
|
|
186
|
+
const { wiring } = makeWiring({ channel, permissionTimeoutSec: 1 });
|
|
187
|
+
await wiring.attach('g1', 'c1', 'claude');
|
|
188
|
+
const decisionP = wiring.requestPermission(binding, { toolName: 'Bash', input: {} });
|
|
189
|
+
await vi.advanceTimersByTimeAsync(1100);
|
|
190
|
+
const decision = await decisionP;
|
|
191
|
+
expect(decision.behavior).toBe('deny');
|
|
192
|
+
expect(decision.message).toContain('timed out');
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
vi.useRealTimers();
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
it('denies when the channel is not wired (no live prompt possible)', async () => {
|
|
199
|
+
const { channel } = fakeChannel();
|
|
200
|
+
const { wiring } = makeWiring({ channel });
|
|
201
|
+
// No attach() → not wired.
|
|
202
|
+
const decision = await wiring.requestPermission(binding, { toolName: 'Bash', input: {} });
|
|
203
|
+
expect(decision.behavior).toBe('deny');
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
describe('SessionWiring rendering + sendFile', () => {
|
|
207
|
+
it('subscribing renders an emitted AgentEvent to the channel; detach stops it', async () => {
|
|
208
|
+
const { channel, sent } = fakeChannel();
|
|
209
|
+
const { wiring, eventBus } = makeWiring({ channel });
|
|
210
|
+
await wiring.attach('g1', 'c1', 'claude');
|
|
211
|
+
eventBus.emit('g1', 'c1', { kind: 'error', message: 'boom', retryable: false });
|
|
212
|
+
await Promise.resolve();
|
|
213
|
+
expect(sent.some((m) => (m.content ?? '').includes('boom'))).toBe(true);
|
|
214
|
+
const before = sent.length;
|
|
215
|
+
wiring.detach('g1', 'c1');
|
|
216
|
+
eventBus.emit('g1', 'c1', { kind: 'error', message: 'again', retryable: false });
|
|
217
|
+
await Promise.resolve();
|
|
218
|
+
expect(sent.length).toBe(before); // no new render after detach
|
|
219
|
+
});
|
|
220
|
+
it('detach cancels an armed stream timer: no late channel.send after stop', async () => {
|
|
221
|
+
vi.useFakeTimers();
|
|
222
|
+
try {
|
|
223
|
+
const { channel, sent } = fakeChannel();
|
|
224
|
+
const { wiring, eventBus } = makeWiring({ channel });
|
|
225
|
+
await wiring.attach('g1', 'c1', 'claude');
|
|
226
|
+
// A streaming text delta arms the debounce timer (default 1s) but does not
|
|
227
|
+
// send yet. This is a turn in flight.
|
|
228
|
+
eventBus.emit('g1', 'c1', { kind: 'text', text: 'partial…', delta: true });
|
|
229
|
+
const before = sent.length;
|
|
230
|
+
// /stop mid-stream: detach must cancel the armed timer.
|
|
231
|
+
wiring.detach('g1', 'c1');
|
|
232
|
+
// Fire everything the debounce could have scheduled.
|
|
233
|
+
await vi.advanceTimersByTimeAsync(5000);
|
|
234
|
+
// No streaming embed was ever posted — the timer was cancelled on detach.
|
|
235
|
+
expect(sent.length).toBe(before);
|
|
236
|
+
}
|
|
237
|
+
finally {
|
|
238
|
+
vi.useRealTimers();
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
it('sendFileFor posts the confined file to the bound channel', async () => {
|
|
242
|
+
const { channel, sent } = fakeChannel();
|
|
243
|
+
const { wiring } = makeWiring({ channel });
|
|
244
|
+
await wiring.attach('g1', 'c1', 'claude');
|
|
245
|
+
const send = wiring.sendFileFor('g1', 'c1');
|
|
246
|
+
const msg = await send('/ws/out.txt', 'out.txt');
|
|
247
|
+
expect(msg).toContain('out.txt');
|
|
248
|
+
const fileMsg = sent.find((m) => m.files && m.files.length > 0);
|
|
249
|
+
expect(fileMsg?.files?.[0]).toEqual({ path: '/ws/out.txt', name: 'out.txt' });
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
//# sourceMappingURL=wiring.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wiring.test.js","sourceRoot":"","sources":["../../src/discord/wiring.test.ts"],"names":[],"mappings":"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"}
|