kimaki 1.3.0 → 1.5.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/assets/fonts/Geist-Bold.ttf +0 -0
- package/assets/fonts/Geist-Regular.ttf +0 -0
- package/assets/fonts/GeistMono-Regular.ttf +0 -0
- package/assets/fonts/OFL.txt +92 -0
- package/dist/agent-ui.e2e.test.js +27 -13
- package/dist/agent-ui.e2e.test.js.map +1 -1
- package/dist/agent-ui.js +2 -1
- package/dist/agent-ui.js.map +1 -1
- package/dist/analytics.e2e.test.js +1 -1
- package/dist/analytics.e2e.test.js.map +1 -1
- package/dist/autostart.d.ts +4 -0
- package/dist/autostart.js +55 -10
- package/dist/autostart.js.map +1 -1
- package/dist/bot.d.ts +13 -6
- package/dist/bot.js +17 -15
- package/dist/bot.js.map +1 -1
- package/dist/btw.e2e.test.js +3 -3
- package/dist/builtin-skills.d.ts +3 -0
- package/dist/builtin-skills.js +29 -0
- package/dist/builtin-skills.js.map +1 -1
- package/dist/builtin-skills.test.js +72 -1
- package/dist/builtin-skills.test.js.map +1 -1
- package/dist/catalog-refresh.e2e.test.js +2 -2
- package/dist/cli/bot.d.ts +1 -1
- package/dist/cli/bot.js +33 -18
- package/dist/cli/bot.js.map +1 -1
- package/dist/cli/discord.js +32 -6
- package/dist/cli/discord.js.map +1 -1
- package/dist/cli/schedule.js +23 -9
- package/dist/cli/schedule.js.map +1 -1
- package/dist/cli/session.js +37 -12
- package/dist/cli/session.js.map +1 -1
- package/dist/cli/shared.d.ts +5 -3
- package/dist/cli/shared.js +45 -10
- package/dist/cli/shared.js.map +1 -1
- package/dist/cli-discord.e2e.test.js +13 -8
- package/dist/cli-discord.e2e.test.js.map +1 -1
- package/dist/cli-lock.e2e.test.js +87 -20
- package/dist/cli-lock.e2e.test.js.map +1 -1
- package/dist/cli-read.e2e.test.js +18 -9
- package/dist/cli-read.e2e.test.js.map +1 -1
- package/dist/cli-sessions.e2e.test.js +4 -3
- package/dist/cli-sessions.e2e.test.js.map +1 -1
- package/dist/cli.js +2 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/preference-commands.d.ts +4 -0
- package/dist/commands/preference-commands.js +38 -4
- package/dist/commands/preference-commands.js.map +1 -1
- package/dist/commands/session-commands.js +11 -8
- package/dist/commands/session-commands.js.map +1 -1
- package/dist/commands/thread-commands.js +94 -24
- package/dist/commands/thread-commands.js.map +1 -1
- package/dist/commands/worktree-commands.d.ts +5 -6
- package/dist/commands/worktree-commands.js +8 -7
- package/dist/commands/worktree-commands.js.map +1 -1
- package/dist/commands.e2e.test.js +25 -21
- package/dist/commands.e2e.test.js.map +1 -1
- package/dist/coordinator-agents.e2e.test.d.ts +1 -0
- package/dist/coordinator-agents.e2e.test.js +113 -0
- package/dist/coordinator-agents.e2e.test.js.map +1 -0
- package/dist/coordinator-idle.e2e.test.d.ts +1 -0
- package/dist/coordinator-idle.e2e.test.js +100 -0
- package/dist/coordinator-idle.e2e.test.js.map +1 -0
- package/dist/coordinator-queue.e2e.test.d.ts +1 -0
- package/dist/coordinator-queue.e2e.test.js +244 -0
- package/dist/coordinator-queue.e2e.test.js.map +1 -0
- package/dist/coordinator-warmup.e2e.test.d.ts +1 -0
- package/dist/coordinator-warmup.e2e.test.js +131 -0
- package/dist/coordinator-warmup.e2e.test.js.map +1 -0
- package/dist/coordinator.d.ts +6 -0
- package/dist/coordinator.e2e.test.d.ts +1 -0
- package/dist/coordinator.e2e.test.js +505 -0
- package/dist/coordinator.e2e.test.js.map +1 -0
- package/dist/coordinator.js +164 -0
- package/dist/coordinator.js.map +1 -0
- package/dist/db.d.ts +78 -3
- package/dist/db.test.js +1 -0
- package/dist/db.test.js.map +1 -1
- package/dist/diagram.d.ts +11 -0
- package/dist/diagram.js +232 -0
- package/dist/diagram.js.map +1 -0
- package/dist/diagram.test.d.ts +1 -0
- package/dist/diagram.test.js +52 -0
- package/dist/diagram.test.js.map +1 -0
- package/dist/duration.js +13 -5
- package/dist/duration.js.map +1 -1
- package/dist/effects.d.ts +6 -1
- package/dist/effects.js +97 -26
- package/dist/effects.js.map +1 -1
- package/dist/event-loop.d.ts +2 -3
- package/dist/event-loop.js +74 -44
- package/dist/event-loop.js.map +1 -1
- package/dist/format-parts.d.ts +21 -4
- package/dist/format-parts.js +40 -23
- package/dist/format-parts.js.map +1 -1
- package/dist/gateway-replay.e2e.test.js +4 -4
- package/dist/harness.e2e.test.js +2 -2
- package/dist/harness.e2e.test.js.map +1 -1
- package/dist/hello-thread.e2e.test.js +56 -5
- package/dist/hello-thread.e2e.test.js.map +1 -1
- package/dist/ingress.js +47 -12
- package/dist/ingress.js.map +1 -1
- package/dist/interaction-context.d.ts +4 -4
- package/dist/interaction-context.js +9 -7
- package/dist/interaction-context.js.map +1 -1
- package/dist/interrupt.e2e.test.js +8 -5
- package/dist/interrupt.e2e.test.js.map +1 -1
- package/dist/legacy.e2e.test.js +1 -1
- package/dist/lock-routes.d.ts +83 -31
- package/dist/lock-routes.js +68 -8
- package/dist/lock-routes.js.map +1 -1
- package/dist/lock-server.d.ts +3 -2
- package/dist/lock-server.js +23 -7
- package/dist/lock-server.js.map +1 -1
- package/dist/main.d.ts +6 -3
- package/dist/main.js +14 -4
- package/dist/main.js.map +1 -1
- package/dist/mainmast-idle.e2e.test.d.ts +1 -0
- package/dist/mainmast-idle.e2e.test.js +99 -0
- package/dist/mainmast-idle.e2e.test.js.map +1 -0
- package/dist/mainmast-queue.e2e.test.d.ts +1 -0
- package/dist/mainmast-queue.e2e.test.js +244 -0
- package/dist/mainmast-queue.e2e.test.js.map +1 -0
- package/dist/mainmast-warmup.e2e.test.d.ts +1 -0
- package/dist/mainmast-warmup.e2e.test.js +130 -0
- package/dist/mainmast-warmup.e2e.test.js.map +1 -0
- package/dist/mainmast.d.ts +6 -0
- package/dist/mainmast.e2e.test.d.ts +1 -0
- package/dist/mainmast.e2e.test.js +428 -0
- package/dist/mainmast.e2e.test.js.map +1 -0
- package/dist/mainmast.js +156 -0
- package/dist/mainmast.js.map +1 -0
- package/dist/markdown/markdown.test.js +14 -4
- package/dist/markdown/markdown.test.js.map +1 -1
- package/dist/markdown/render-markdown.js +9 -6
- package/dist/markdown/render-markdown.js.map +1 -1
- package/dist/onboarding-cli.e2e.test.js +7 -6
- package/dist/onboarding-cli.e2e.test.js.map +1 -1
- package/dist/onboarding.d.ts +3 -10
- package/dist/onboarding.e2e.test.js +8 -10
- package/dist/onboarding.e2e.test.js.map +1 -1
- package/dist/onboarding.js +14 -24
- package/dist/onboarding.js.map +1 -1
- package/dist/permissions.e2e.test.js +3 -3
- package/dist/plugin/coordinator.d.ts +4 -0
- package/dist/plugin/coordinator.js +389 -0
- package/dist/plugin/coordinator.js.map +1 -0
- package/dist/plugin/index.js +93 -61
- package/dist/plugin/index.js.map +1 -1
- package/dist/plugin/mainmast.d.ts +4 -0
- package/dist/plugin/mainmast.js +379 -0
- package/dist/plugin/mainmast.js.map +1 -0
- package/dist/plugin.e2e.test.js +1 -1
- package/dist/preference-commands.e2e.test.js +103 -24
- package/dist/preference-commands.e2e.test.js.map +1 -1
- package/dist/project.d.ts +1 -1
- package/dist/prompt.d.ts +22 -9
- package/dist/prompt.js +64 -48
- package/dist/prompt.js.map +1 -1
- package/dist/questions.e2e.test.js +13 -13
- package/dist/queue-sources.e2e.test.d.ts +1 -0
- package/dist/queue-sources.e2e.test.js +168 -0
- package/dist/queue-sources.e2e.test.js.map +1 -0
- package/dist/queue.d.ts +9 -1
- package/dist/queue.e2e.test.js +48 -30
- package/dist/queue.e2e.test.js.map +1 -1
- package/dist/queue.js +97 -24
- package/dist/queue.js.map +1 -1
- package/dist/queue.test.d.ts +1 -0
- package/dist/queue.test.js +57 -0
- package/dist/queue.test.js.map +1 -0
- package/dist/remote-send.e2e.test.js +6 -6
- package/dist/remote-send.js +2 -1
- package/dist/remote-send.js.map +1 -1
- package/dist/renderer.e2e.test.js +7 -7
- package/dist/restart.e2e.test.js +2 -2
- package/dist/routines.e2e.test.js +2 -2
- package/dist/scheduler.d.ts +3 -3
- package/dist/scheduler.e2e.test.js +43 -20
- package/dist/scheduler.e2e.test.js.map +1 -1
- package/dist/scheduler.js +4 -2
- package/dist/scheduler.js.map +1 -1
- package/dist/schema-sql.d.ts +1 -1
- package/dist/schema-sql.js +1 -1
- package/dist/schema-sql.js.map +1 -1
- package/dist/schema.d.ts +152 -6
- package/dist/schema.js +12 -0
- package/dist/schema.js.map +1 -1
- package/dist/session-commands.e2e.test.js +36 -11
- package/dist/session-commands.e2e.test.js.map +1 -1
- package/dist/session-events.d.ts +18 -6
- package/dist/session-events.js +175 -27
- package/dist/session-events.js.map +1 -1
- package/dist/session-ops.e2e.test.js +63 -12
- package/dist/session-ops.e2e.test.js.map +1 -1
- package/dist/sessions.d.ts +44 -21
- package/dist/sessions.js +177 -65
- package/dist/sessions.js.map +1 -1
- package/dist/shell.e2e.test.js +4 -4
- package/dist/slash-commands.js +6 -4
- package/dist/slash-commands.js.map +1 -1
- package/dist/slash-commands.test.js +2 -0
- package/dist/slash-commands.test.js.map +1 -1
- package/dist/sleep.e2e.test.js +29 -14
- package/dist/sleep.e2e.test.js.map +1 -1
- package/dist/sleeps.d.ts +2 -1
- package/dist/sleeps.js +21 -3
- package/dist/sleeps.js.map +1 -1
- package/dist/store.d.ts +1 -0
- package/dist/store.js +3 -2
- package/dist/store.js.map +1 -1
- package/dist/system-prompt.d.ts +5 -15
- package/dist/system-prompt.js +180 -131
- package/dist/system-prompt.js.map +1 -1
- package/dist/test/generate-log-plugin/index.d.ts +3 -0
- package/dist/test/generate-log-plugin/index.js +15 -0
- package/dist/test/generate-log-plugin/index.js.map +1 -0
- package/dist/test/harness.d.ts +20 -2
- package/dist/test/harness.js +51 -11
- package/dist/test/harness.js.map +1 -1
- package/dist/test/replay.js +11 -8
- package/dist/test/replay.js.map +1 -1
- package/dist/thread-reducer.d.ts +29 -6
- package/dist/thread-reducer.fixtures.test.js +58 -51
- package/dist/thread-reducer.fixtures.test.js.map +1 -1
- package/dist/thread-reducer.js +109 -50
- package/dist/thread-reducer.js.map +1 -1
- package/dist/thread-reducer.test.js +190 -48
- package/dist/thread-reducer.test.js.map +1 -1
- package/dist/tunnel.d.ts +9 -0
- package/dist/tunnel.js +117 -28
- package/dist/tunnel.js.map +1 -1
- package/dist/tunnel.test.js +81 -5
- package/dist/tunnel.test.js.map +1 -1
- package/dist/voice-attachments.e2e.test.js +108 -18
- package/dist/voice-attachments.e2e.test.js.map +1 -1
- package/dist/voice-call.e2e.test.js +9 -9
- package/dist/voice-call.js +6 -6
- package/dist/voice-call.js.map +1 -1
- package/dist/voice-openai.e2e.test.js +59 -23
- package/dist/voice-openai.e2e.test.js.map +1 -1
- package/dist/voice.d.ts +1 -1
- package/dist/voice.js +72 -24
- package/dist/voice.js.map +1 -1
- package/dist/working-directory.e2e.test.js +11 -11
- package/dist/working-directory.e2e.test.js.map +1 -1
- package/package.json +11 -5
- package/skills/kimaki-tunnel/SKILL.md +3 -2
- package/src/agent-ui.e2e.test.ts +27 -13
- package/src/agent-ui.ts +2 -1
- package/src/analytics.e2e.test.ts +1 -1
- package/src/autostart.ts +55 -10
- package/src/bot.ts +36 -19
- package/src/btw.e2e.test.ts +3 -3
- package/src/builtin-skills.test.ts +73 -1
- package/src/builtin-skills.ts +33 -0
- package/src/catalog-refresh.e2e.test.ts +2 -2
- package/src/cli/bot.ts +30 -19
- package/src/cli/discord.ts +30 -6
- package/src/cli/schedule.ts +23 -9
- package/src/cli/session.ts +35 -12
- package/src/cli/shared.ts +43 -8
- package/src/cli-discord.e2e.test.ts +13 -8
- package/src/cli-lock.e2e.test.ts +88 -20
- package/src/cli-read.e2e.test.ts +17 -8
- package/src/cli-sessions.e2e.test.ts +4 -3
- package/src/cli.ts +2 -0
- package/src/commands/preference-commands.ts +38 -4
- package/src/commands/session-commands.ts +12 -7
- package/src/commands/thread-commands.ts +92 -24
- package/src/commands/worktree-commands.ts +10 -9
- package/src/commands.e2e.test.ts +25 -21
- package/src/coordinator-agents.e2e.test.ts +145 -0
- package/src/coordinator-idle.e2e.test.ts +122 -0
- package/src/coordinator-queue.e2e.test.ts +279 -0
- package/src/coordinator-warmup.e2e.test.ts +158 -0
- package/src/coordinator.e2e.test.ts +545 -0
- package/src/coordinator.ts +168 -0
- package/src/db.test.ts +1 -0
- package/src/diagram.test.ts +56 -0
- package/src/diagram.ts +243 -0
- package/src/duration.ts +13 -5
- package/src/effects.ts +99 -27
- package/src/event-loop.ts +71 -45
- package/src/format-parts.ts +53 -22
- package/src/gateway-replay.e2e.test.ts +4 -4
- package/src/harness.e2e.test.ts +2 -2
- package/src/hello-thread.e2e.test.ts +59 -5
- package/src/ingress.ts +44 -11
- package/src/interaction-context.ts +12 -11
- package/src/interrupt.e2e.test.ts +8 -5
- package/src/legacy.e2e.test.ts +1 -1
- package/src/lock-routes.ts +62 -8
- package/src/lock-server.ts +23 -7
- package/src/main.ts +26 -10
- package/src/markdown/markdown.test.ts +15 -4
- package/src/markdown/render-markdown.ts +9 -6
- package/src/onboarding-cli.e2e.test.ts +7 -6
- package/src/onboarding.e2e.test.ts +8 -10
- package/src/onboarding.ts +13 -29
- package/src/permissions.e2e.test.ts +3 -3
- package/src/plugin/coordinator.ts +401 -0
- package/src/plugin/index.ts +86 -59
- package/src/plugin.e2e.test.ts +1 -1
- package/src/preference-commands.e2e.test.ts +103 -23
- package/src/prompt.ts +87 -43
- package/src/questions.e2e.test.ts +13 -13
- package/src/queue-sources.e2e.test.ts +197 -0
- package/src/queue.e2e.test.ts +48 -30
- package/src/queue.test.ts +59 -0
- package/src/queue.ts +104 -24
- package/src/remote-send.e2e.test.ts +6 -6
- package/src/remote-send.ts +2 -1
- package/src/renderer.e2e.test.ts +7 -7
- package/src/restart.e2e.test.ts +2 -2
- package/src/routines.e2e.test.ts +2 -2
- package/src/scheduler.e2e.test.ts +45 -19
- package/src/scheduler.ts +4 -2
- package/src/schema-sql.ts +1 -1
- package/src/schema.ts +13 -0
- package/src/session-commands.e2e.test.ts +38 -11
- package/src/session-events.ts +168 -25
- package/src/session-ops.e2e.test.ts +64 -11
- package/src/sessions.ts +217 -67
- package/src/shell.e2e.test.ts +4 -4
- package/src/slash-commands.test.ts +2 -0
- package/src/slash-commands.ts +6 -4
- package/src/sleep.e2e.test.ts +30 -13
- package/src/sleeps.ts +20 -3
- package/src/store.ts +5 -2
- package/src/system-prompt.ts +190 -134
- package/src/test/generate-log-plugin/index.ts +16 -0
- package/src/test/harness.ts +77 -17
- package/src/test/replay.ts +12 -7
- package/src/thread-reducer.fixtures.test.ts +58 -51
- package/src/thread-reducer.test.ts +199 -49
- package/src/thread-reducer.ts +135 -66
- package/src/tunnel.test.ts +81 -5
- package/src/tunnel.ts +124 -25
- package/src/voice-attachments.e2e.test.ts +110 -18
- package/src/voice-call.e2e.test.ts +9 -9
- package/src/voice-call.ts +6 -6
- package/src/voice-openai.e2e.test.ts +64 -24
- package/src/voice.ts +93 -26
- package/src/working-directory.e2e.test.ts +11 -11
- package/src/message-tool.e2e.test.ts +0 -45
package/src/tunnel.ts
CHANGED
|
@@ -5,16 +5,22 @@
|
|
|
5
5
|
|
|
6
6
|
import crypto from 'node:crypto'
|
|
7
7
|
import { execFile, spawn, type ChildProcess } from 'node:child_process'
|
|
8
|
+
import http from 'node:http'
|
|
8
9
|
import net from 'node:net'
|
|
9
10
|
import { setTimeout as sleep } from 'node:timers/promises'
|
|
10
11
|
import { promisify, stripVTControlCharacters } from 'node:util'
|
|
11
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
create as createOpenTunnel,
|
|
14
|
+
isValidRouteName,
|
|
15
|
+
OpenTunnelClientError,
|
|
16
|
+
publicUrl,
|
|
17
|
+
type OpenTunnelClientEvent,
|
|
18
|
+
type OpenTunnelConnection,
|
|
19
|
+
} from '@opentunnel/client'
|
|
12
20
|
|
|
13
21
|
const execFileAsync = promisify(execFile)
|
|
14
22
|
|
|
15
|
-
const ROUTE_NAME = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/
|
|
16
23
|
const PORT_WAIT_MS = 30_000
|
|
17
|
-
const ATTACH_TIMEOUT_MS = 60_000
|
|
18
24
|
const CHILD_KILL_GRACE_MS = 5_000
|
|
19
25
|
|
|
20
26
|
// Only lines with host:port, so "Port 5173 is in use" does not match.
|
|
@@ -31,12 +37,28 @@ export function detectPortFromText(text: string): number | null {
|
|
|
31
37
|
return null
|
|
32
38
|
}
|
|
33
39
|
|
|
40
|
+
// Traforo accepted any tunnel ID, so map it to a DNS label instead of failing: `My_App` becomes `my-app`.
|
|
41
|
+
function toRouteLabel(raw: string): string {
|
|
42
|
+
const label = raw.toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 63).replace(/-+$/, '')
|
|
43
|
+
return isValidRouteName(label) ? label : ''
|
|
44
|
+
}
|
|
45
|
+
|
|
34
46
|
// Explicit name, then the tuistory session ID (stable URL across restarts of one session), then random.
|
|
35
47
|
export function routeName({ name, tuistorySessionId }: { name?: string; tuistorySessionId?: string }): string | Error {
|
|
36
|
-
if (name) return
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
48
|
+
if (name) return toRouteLabel(name) || new Error(`Invalid tunnel ID ${JSON.stringify(name)}: use letters, digits and dashes`)
|
|
49
|
+
return toRouteLabel(tuistorySessionId ?? '') || crypto.randomBytes(8).toString('hex')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Parses `netstat -ano` output on Windows: `TCP 0.0.0.0:5173 0.0.0.0:0 LISTENING 1234`.
|
|
53
|
+
export function parseNetstatListeningPids({ stdout, port }: { stdout: string; port: number }): number[] {
|
|
54
|
+
const pids = new Set<number>()
|
|
55
|
+
for (const line of stdout.split('\n')) {
|
|
56
|
+
const [protocol, local, , state, pid] = line.trim().split(/\s+/)
|
|
57
|
+
if (protocol !== 'TCP' || state !== 'LISTENING' || !local?.endsWith(`:${port}`)) continue
|
|
58
|
+
const parsed = Number(pid)
|
|
59
|
+
if (Number.isInteger(parsed) && parsed > 0) pids.add(parsed)
|
|
60
|
+
}
|
|
61
|
+
return [...pids]
|
|
40
62
|
}
|
|
41
63
|
|
|
42
64
|
function isPortInUse({ port, host }: { port: number; host: string }): Promise<boolean> {
|
|
@@ -55,6 +77,10 @@ async function waitForPort({ port, host, signal }: { port: number; host: string;
|
|
|
55
77
|
}
|
|
56
78
|
|
|
57
79
|
async function listeningPids(port: number): Promise<number[]> {
|
|
80
|
+
if (process.platform === 'win32') {
|
|
81
|
+
const { stdout } = await execFileAsync('netstat', ['-ano'], { timeout: 5_000 }).catch(() => ({ stdout: '' }))
|
|
82
|
+
return parseNetstatListeningPids({ stdout, port })
|
|
83
|
+
}
|
|
58
84
|
const { stdout } = await execFileAsync('lsof', ['-ti', `tcp:${port}`, '-sTCP:LISTEN'], { timeout: 5_000 }).catch(() => ({ stdout: '' }))
|
|
59
85
|
return stdout.split('\n').map(Number).filter((pid) => Number.isInteger(pid) && pid > 0)
|
|
60
86
|
}
|
|
@@ -107,6 +133,69 @@ function killChild(child: ChildProcess | undefined, signal: NodeJS.Signals = 'SI
|
|
|
107
133
|
|
|
108
134
|
const log = (line: string) => process.stderr.write(`${line}\n`)
|
|
109
135
|
|
|
136
|
+
function logTunnelEvent(event: OpenTunnelClientEvent) {
|
|
137
|
+
if (event.type === 'disconnected') log(`Tunnel disconnected: ${event.reason}`)
|
|
138
|
+
if (event.type === 'connected') log('Tunnel connected')
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function forwardedHeaders({ headers, target }: { headers: http.IncomingHttpHeaders; target: string }): http.OutgoingHttpHeaders {
|
|
142
|
+
return {
|
|
143
|
+
...headers,
|
|
144
|
+
host: target,
|
|
145
|
+
'x-forwarded-host': headers['x-forwarded-host'] ?? headers.host,
|
|
146
|
+
'x-forwarded-proto': headers['x-forwarded-proto'] ?? 'https',
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function rawResponseHead(response: http.IncomingMessage): string {
|
|
151
|
+
let head = `HTTP/1.1 ${response.statusCode} ${response.statusMessage}\r\n`
|
|
152
|
+
for (let i = 0; i < response.rawHeaders.length; i += 2) head += `${response.rawHeaders[i]}: ${response.rawHeaders[i + 1]}\r\n`
|
|
153
|
+
return `${head}\r\n`
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// opentunnel forwards the raw bytes, so the app would see the public Host header. Vite and other dev
|
|
157
|
+
// servers reject unknown hosts (DNS rebinding protection) with 403. This loopback proxy sends
|
|
158
|
+
// Host: localhost:<port> and keeps the public host in X-Forwarded-Host, like traforo did.
|
|
159
|
+
export function startHostProxy({ host, port }: { host: string; port: number }): Promise<http.Server | Error> {
|
|
160
|
+
const target = `${host}:${port}`
|
|
161
|
+
const server = http.createServer((req, res) => {
|
|
162
|
+
const upstream = http.request({ host, port, method: req.method, path: req.url, headers: forwardedHeaders({ headers: req.headers, target }) }, (response) => {
|
|
163
|
+
res.writeHead(response.statusCode ?? 502, response.statusMessage, response.rawHeaders)
|
|
164
|
+
response.pipe(res)
|
|
165
|
+
})
|
|
166
|
+
upstream.on('error', () => {
|
|
167
|
+
if (!res.headersSent) res.writeHead(502)
|
|
168
|
+
res.end()
|
|
169
|
+
})
|
|
170
|
+
req.pipe(upstream)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
// WebSocket upgrades (Vite HMR): replay the handshake upstream, then pipe both sockets.
|
|
174
|
+
server.on('upgrade', (req, socket, head) => {
|
|
175
|
+
const upstream = http.request({ host, port, method: req.method, path: req.url, headers: forwardedHeaders({ headers: req.headers, target }) })
|
|
176
|
+
upstream.on('upgrade', (response, upstreamSocket, upstreamHead) => {
|
|
177
|
+
socket.write(rawResponseHead(response))
|
|
178
|
+
if (upstreamHead.length > 0) socket.write(upstreamHead)
|
|
179
|
+
if (head.length > 0) upstreamSocket.write(head)
|
|
180
|
+
upstreamSocket.on('error', () => socket.destroy())
|
|
181
|
+
socket.on('error', () => upstreamSocket.destroy())
|
|
182
|
+
upstreamSocket.pipe(socket).pipe(upstreamSocket)
|
|
183
|
+
})
|
|
184
|
+
upstream.on('response', (response) => {
|
|
185
|
+
socket.write(rawResponseHead(response))
|
|
186
|
+
response.pipe(socket)
|
|
187
|
+
})
|
|
188
|
+
upstream.on('error', () => socket.destroy())
|
|
189
|
+
socket.on('error', () => upstream.destroy())
|
|
190
|
+
upstream.end()
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
return new Promise((resolve) => {
|
|
194
|
+
server.once('error', (cause) => resolve(new Error('Could not start the tunnel proxy', { cause })))
|
|
195
|
+
server.listen(0, '127.0.0.1', () => resolve(server))
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
|
|
110
199
|
export async function runTunnel(options: {
|
|
111
200
|
port?: number
|
|
112
201
|
command?: string[]
|
|
@@ -127,11 +216,18 @@ export async function runTunnel(options: {
|
|
|
127
216
|
}
|
|
128
217
|
|
|
129
218
|
const client = createOpenTunnel()
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
219
|
+
let waitLogged = false
|
|
220
|
+
const identity = await client.tunnel
|
|
221
|
+
.ensure({
|
|
222
|
+
onProgress: (stage) => {
|
|
223
|
+
if (waitLogged || stage === 'checking-certificate' || stage === 'ready') return
|
|
224
|
+
waitLogged = true
|
|
225
|
+
log('Getting a certificate for the opentunnel.xyz tunnel of this machine (up to a minute)...')
|
|
226
|
+
},
|
|
227
|
+
})
|
|
228
|
+
.catch((cause) => new Error('Could not create the opentunnel tunnel', { cause }))
|
|
133
229
|
if (identity instanceof Error) return identity
|
|
134
|
-
const url =
|
|
230
|
+
const url = publicUrl(route, identity.hostname)
|
|
135
231
|
|
|
136
232
|
let child: ChildProcess | undefined
|
|
137
233
|
let connection: OpenTunnelConnection | undefined
|
|
@@ -186,24 +282,27 @@ export async function runTunnel(options: {
|
|
|
186
282
|
if (!port) return fail(new Error('Could not find the local port. Pass --port'))
|
|
187
283
|
const ready = await waitForPort({ port, host, signal: portSignal })
|
|
188
284
|
if (ready instanceof Error) return fail(ready)
|
|
285
|
+
const proxy = await startHostProxy({ host, port })
|
|
286
|
+
if (proxy instanceof Error) return fail(proxy)
|
|
287
|
+
const proxyAddress = proxy.address()
|
|
288
|
+
if (!proxyAddress || typeof proxyAddress === 'string') return fail(new Error('Tunnel proxy has no TCP address'))
|
|
189
289
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
290
|
+
const attached = await client.tunnel
|
|
291
|
+
.connect({ routes: { [route]: `127.0.0.1:${proxyAddress.port}` }, onRouteConflict: 'fail', onEvent: logTunnelEvent })
|
|
292
|
+
.catch((cause) => {
|
|
293
|
+
if (cause instanceof OpenTunnelClientError && cause.code === 'route_conflict') {
|
|
294
|
+
return new Error(`Tunnel ID ${route} is already used by another process. Pass a different -t`, { cause })
|
|
295
|
+
}
|
|
296
|
+
return new Error('Could not connect the tunnel', { cause })
|
|
297
|
+
})
|
|
194
298
|
if (attached instanceof Error) return fail(attached)
|
|
195
|
-
if (!attached) return fail(new Error(`Tunnel did not attach in ${ATTACH_TIMEOUT_MS / 1000}s. Another process may use the tunnel ID ${route}`))
|
|
196
299
|
connection = attached
|
|
197
300
|
|
|
198
|
-
process.stdout.write(
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
if (event.type === 'disconnected') log(`Tunnel disconnected: ${event.reason}`)
|
|
203
|
-
if (event.type === 'connected') log('Tunnel connected')
|
|
204
|
-
}
|
|
205
|
-
})()
|
|
301
|
+
process.stdout.write(
|
|
302
|
+
`\nLocal: http://${host}:${port}\nTunnel: ${url}\n` +
|
|
303
|
+
`Agents: validate with the Local URL (playwriter, curl), it is much faster. The Tunnel URL is for the user.\n\n`,
|
|
304
|
+
)
|
|
206
305
|
|
|
207
|
-
const closed = await attached.closed.then(() => undefined, (cause
|
|
306
|
+
const closed = await attached.closed.then(() => undefined, (cause) => new Error('Tunnel stopped', { cause }))
|
|
208
307
|
return fail(closed ?? new Error('Tunnel stopped'))
|
|
209
308
|
}
|
|
@@ -39,9 +39,9 @@ const matchers: DeterministicMatcher[] = [
|
|
|
39
39
|
{
|
|
40
40
|
id: 'image',
|
|
41
41
|
priority: 500,
|
|
42
|
-
// The plugin adds the numbered <attachments
|
|
42
|
+
// The plugin adds the numbered <attachments>, the concise reminder and <discord-user> to the model request only.
|
|
43
43
|
when: {
|
|
44
|
-
latestUserTextRegex: 'image-marker[\\s\\S]*<attachments>[\\s\\S]*1\\. dot\\.png \\(inline\\): [^\\n]*\\n2\\. notes\\.txt: [^\\n]*\\n3\\. archive\\.zip: [^\\n]*\\n</attachments>\\n<discord-user ',
|
|
44
|
+
latestUserTextRegex: 'image-marker[\\s\\S]*<attachments>[\\s\\S]*1\\. dot\\.png \\(inline\\): [^\\n]*\\n2\\. notes\\.txt: [^\\n]*\\n3\\. archive\\.zip: [^\\n]*\\n</attachments>\\n<system-reminder>[^\\n]*</system-reminder>\\n<discord-user ',
|
|
45
45
|
rawPromptIncludes: 'image/png',
|
|
46
46
|
},
|
|
47
47
|
then: { parts: textParts('I see an image') },
|
|
@@ -51,6 +51,10 @@ const matchers: DeterministicMatcher[] = [
|
|
|
51
51
|
reply('voice-btw', 'voice btw ok'),
|
|
52
52
|
reply('voice-new', 'voice new session ok'),
|
|
53
53
|
reply('voice-plan', 'voice plan ok'),
|
|
54
|
+
reply('order-start', 'order start ok'),
|
|
55
|
+
// Above order-start: the thread name in <discord-user> repeats that marker.
|
|
56
|
+
{ ...reply('order-voice', 'order voice ok'), priority: 600 },
|
|
57
|
+
{ ...reply('order-text', 'order text ok'), priority: 600 },
|
|
54
58
|
]
|
|
55
59
|
|
|
56
60
|
let server: OpencodeTestServer
|
|
@@ -130,7 +134,7 @@ test('image and text attachments reach the model as files', async () => {
|
|
|
130
134
|
--- from: assistant (TestBot)
|
|
131
135
|
-# *using deterministic-provider/deterministic-v2 ⋅ build*
|
|
132
136
|
I see an image
|
|
133
|
-
-# *
|
|
137
|
+
-# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
134
138
|
`)
|
|
135
139
|
const sessionId = bot.store.getState().roots[thread.id]!
|
|
136
140
|
const messages = await (await server.client()).message.list({ sessionID: sessionId })
|
|
@@ -156,13 +160,78 @@ test('voice in a channel starts a session with the transcription and the spoken
|
|
|
156
160
|
"--- from: user (tommy)
|
|
157
161
|
[attachment: voice-message.ogg]
|
|
158
162
|
--- from: assistant (TestBot)
|
|
159
|
-
» **tommy:** Plan the refactor voice-plan
|
|
163
|
+
-# » **tommy:** Plan the refactor voice-plan
|
|
160
164
|
-# *using deterministic-provider/deterministic-v2 ⋅ plan*
|
|
161
165
|
voice plan ok
|
|
162
|
-
-# *
|
|
166
|
+
-# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2 ⋅ plan*"
|
|
163
167
|
`)
|
|
164
168
|
})
|
|
165
169
|
|
|
170
|
+
test('a text sent while a voice message is transcribed waits for it', async () => {
|
|
171
|
+
const thread = await newThread(() => twin.discord.channel(twin.channelId).user(TEST_USER_ID).sendMessage({ content: 'Start order-start' }))
|
|
172
|
+
await waitForFooter({ discord: twin.discord, threadId: thread.id })
|
|
173
|
+
const user = twin.discord.thread(thread.id).user(TEST_USER_ID)
|
|
174
|
+
await user.sendVoiceMessage({ url: voiceUrl({ transcription: 'Spoken first order-voice', route: 'queue', delayMs: 500 }) })
|
|
175
|
+
await user.sendMessage({ content: 'Typed second order-text' })
|
|
176
|
+
await waitForBotMessageContaining({ discord: twin.discord, threadId: thread.id, text: 'order text ok' })
|
|
177
|
+
await waitForFooter({ discord: twin.discord, threadId: thread.id, count: 2 })
|
|
178
|
+
expect(await twin.discord.thread(thread.id).text()).toMatchInlineSnapshot(`
|
|
179
|
+
"--- from: user (tommy)
|
|
180
|
+
Start order-start
|
|
181
|
+
--- from: assistant (TestBot)
|
|
182
|
+
-# *using deterministic-provider/deterministic-v2 ⋅ build*
|
|
183
|
+
order start ok
|
|
184
|
+
-# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
185
|
+
--- from: user (tommy)
|
|
186
|
+
[attachment: voice-message.ogg]
|
|
187
|
+
Typed second order-text
|
|
188
|
+
--- from: assistant (TestBot)
|
|
189
|
+
-# » **tommy:** Spoken first order-voice
|
|
190
|
+
order text ok
|
|
191
|
+
-# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
192
|
+
`)
|
|
193
|
+
const sessionId = bot.store.getState().roots[thread.id]!
|
|
194
|
+
const messages = await (await server.client()).message.list({ sessionID: sessionId, order: 'asc' })
|
|
195
|
+
expect(messages.data.flatMap((message) => (message.type === 'user' ? [message.text] : []))).toMatchInlineSnapshot(`
|
|
196
|
+
[
|
|
197
|
+
"Start order-start",
|
|
198
|
+
"Spoken first order-voice",
|
|
199
|
+
"Typed second order-text",
|
|
200
|
+
]
|
|
201
|
+
`)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
// https://github.com/remorses/kimaki/issues/246
|
|
205
|
+
test('a long voice transcript is echoed in full over several messages, in a channel and in a thread', async () => {
|
|
206
|
+
const words = Array.from({ length: 450 }, (_, index) => `w${index}`)
|
|
207
|
+
const transcription = `Long voice-steer ${words.join(' ')}`
|
|
208
|
+
const voice = { url: voiceUrl({ transcription, route: 'steer' }) }
|
|
209
|
+
const thread = await newThread(() => twin.discord.channel(twin.channelId).user(TEST_USER_ID).sendVoiceMessage(voice))
|
|
210
|
+
await waitForFooter({ discord: twin.discord, threadId: thread.id })
|
|
211
|
+
await twin.discord.thread(thread.id).user(TEST_USER_ID).sendVoiceMessage(voice)
|
|
212
|
+
await waitForFooter({ discord: twin.discord, threadId: thread.id, count: 2 })
|
|
213
|
+
const text = await twin.discord.thread(thread.id).text()
|
|
214
|
+
expect(text.replace(/w\d+( w\d+)*/g, (run) => `<${run.split(' ')[0]}..${run.split(' ').at(-1)}>`)).toMatchInlineSnapshot(`
|
|
215
|
+
"--- from: user (tommy)
|
|
216
|
+
[attachment: voice-message.ogg]
|
|
217
|
+
--- from: assistant (TestBot)
|
|
218
|
+
-# » **tommy:** Long voice-steer <w0..w414>
|
|
219
|
+
-# <w415..w449>
|
|
220
|
+
-# *using deterministic-provider/deterministic-v2 ⋅ build*
|
|
221
|
+
voice steer ok
|
|
222
|
+
-# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
223
|
+
--- from: user (tommy)
|
|
224
|
+
[attachment: voice-message.ogg]
|
|
225
|
+
--- from: assistant (TestBot)
|
|
226
|
+
-# » **tommy:** Long voice-steer <w0..w414>
|
|
227
|
+
-# <w415..w449>
|
|
228
|
+
voice steer ok
|
|
229
|
+
-# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
230
|
+
`)
|
|
231
|
+
const echoed = (await twin.discord.thread(thread.id).getMessages()).map((message) => message.content).filter((content) => /\bw\d/.test(content))
|
|
232
|
+
expect(echoed.join(' ').replaceAll('-# ', '')).toBe(`» **tommy:** ${transcription} » **tommy:** ${transcription}`)
|
|
233
|
+
})
|
|
234
|
+
|
|
166
235
|
// https://github.com/remorses/kimaki/issues/235
|
|
167
236
|
test('uploaded audio files with odd Discord media types are transcribed', async () => {
|
|
168
237
|
const user = twin.discord.channel(twin.channelId).user(TEST_USER_ID)
|
|
@@ -179,10 +248,10 @@ test('uploaded audio files with odd Discord media types are transcribed', async
|
|
|
179
248
|
"--- from: user (tommy)
|
|
180
249
|
[attachment: memo.m4a]
|
|
181
250
|
--- from: assistant (TestBot)
|
|
182
|
-
» **tommy:** From m4a voice-steer
|
|
251
|
+
-# » **tommy:** From m4a voice-steer
|
|
183
252
|
-# *using deterministic-provider/deterministic-v2 ⋅ build*
|
|
184
253
|
voice steer ok
|
|
185
|
-
-# *
|
|
254
|
+
-# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
186
255
|
`)
|
|
187
256
|
expect([m4a.name, mp3.name]).toMatchInlineSnapshot(`
|
|
188
257
|
[
|
|
@@ -217,7 +286,7 @@ test('voice in a busy thread: queue waits, btw forks, new-session starts a threa
|
|
|
217
286
|
await waitForFooter({ discord: twin.discord, threadId: thread.id, count: 2 })
|
|
218
287
|
|
|
219
288
|
const hide = (text: string) => text.replace(/<#\d+>/g, '<#THREAD>')
|
|
220
|
-
expect(hide(await twin.discord.thread(thread.id).text())).toMatchInlineSnapshot(`
|
|
289
|
+
expect(hide(await twin.discord.thread(thread.id).text({ showReplies: true }))).toMatchInlineSnapshot(`
|
|
221
290
|
"--- from: user (tommy)
|
|
222
291
|
Long work slow-marker
|
|
223
292
|
--- from: assistant (TestBot)
|
|
@@ -225,23 +294,30 @@ test('voice in a busy thread: queue waits, btw forks, new-session starts a threa
|
|
|
225
294
|
--- from: user (tommy)
|
|
226
295
|
[attachment: voice-message.ogg]
|
|
227
296
|
--- from: assistant (TestBot)
|
|
228
|
-
|
|
229
|
-
-#
|
|
297
|
+
[reply to tommy: ]
|
|
298
|
+
-# » **tommy:** Afterwards voice-queue
|
|
299
|
+
[reply to tommy: ]
|
|
300
|
+
-# Queued
|
|
230
301
|
--- from: user (tommy)
|
|
231
302
|
[attachment: voice-message.ogg]
|
|
232
303
|
--- from: assistant (TestBot)
|
|
233
|
-
|
|
304
|
+
[reply to tommy: ]
|
|
305
|
+
-# » **tommy:** Side voice-btw
|
|
306
|
+
[reply to tommy: ]
|
|
234
307
|
Session forked! Continue in <#THREAD>
|
|
235
308
|
--- from: user (tommy)
|
|
236
309
|
[attachment: voice-message.ogg]
|
|
237
310
|
--- from: assistant (TestBot)
|
|
238
|
-
|
|
311
|
+
[reply to tommy: ]
|
|
312
|
+
-# » **tommy:** Fresh start voice-new
|
|
313
|
+
[reply to tommy: ]
|
|
239
314
|
Started a new session in <#THREAD>
|
|
240
315
|
slow-done
|
|
241
|
-
-# *
|
|
242
|
-
|
|
316
|
+
-# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
317
|
+
[reply to tommy: ]
|
|
318
|
+
-# Queued message sent
|
|
243
319
|
voice queue ok
|
|
244
|
-
-# *
|
|
320
|
+
-# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
245
321
|
`)
|
|
246
322
|
expect([btw.name, fresh.name]).toMatchInlineSnapshot(`
|
|
247
323
|
[
|
|
@@ -254,14 +330,14 @@ test('voice in a busy thread: queue waits, btw forks, new-session starts a threa
|
|
|
254
330
|
Reusing context from <#THREAD> to answer prompt...
|
|
255
331
|
Side voice-btw
|
|
256
332
|
voice btw ok
|
|
257
|
-
-# *
|
|
333
|
+
-# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
258
334
|
`)
|
|
259
335
|
expect(hide(await twin.discord.thread(fresh.id).text())).toMatchInlineSnapshot(`
|
|
260
336
|
"--- from: assistant (TestBot)
|
|
261
|
-
» **tommy:** Fresh start voice-new
|
|
337
|
+
-# » **tommy:** Fresh start voice-new
|
|
262
338
|
-# *using deterministic-provider/deterministic-v2 ⋅ build*
|
|
263
339
|
voice new session ok
|
|
264
|
-
-# *
|
|
340
|
+
-# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
265
341
|
`)
|
|
266
342
|
// The transcription request offers btw and queue only inside a session thread.
|
|
267
343
|
expect(gemini.requests.map((request) => request.routes)).toMatchInlineSnapshot(`
|
|
@@ -270,6 +346,22 @@ test('voice in a busy thread: queue waits, btw forks, new-session starts a threa
|
|
|
270
346
|
"steer",
|
|
271
347
|
"new-session",
|
|
272
348
|
],
|
|
349
|
+
[
|
|
350
|
+
"steer",
|
|
351
|
+
"queue",
|
|
352
|
+
"btw",
|
|
353
|
+
"new-session",
|
|
354
|
+
],
|
|
355
|
+
[
|
|
356
|
+
"steer",
|
|
357
|
+
"new-session",
|
|
358
|
+
],
|
|
359
|
+
[
|
|
360
|
+
"steer",
|
|
361
|
+
"queue",
|
|
362
|
+
"btw",
|
|
363
|
+
"new-session",
|
|
364
|
+
],
|
|
273
365
|
[
|
|
274
366
|
"steer",
|
|
275
367
|
"new-session",
|
|
@@ -136,12 +136,12 @@ test('a user joins and speaks, the backend runs kimaki commands and the model sa
|
|
|
136
136
|
// Thread IDs are snowflakes: replace them for a stable snapshot.
|
|
137
137
|
expect((await twin.discord.channel(voiceChannelId).text()).replace(/<#\d+>/g, '<#thread>')).toMatchInlineSnapshot(`
|
|
138
138
|
"--- from: assistant (TestBot)
|
|
139
|
-
-#
|
|
139
|
+
-# voice call started ⋅ gpt-live ⋅ cove
|
|
140
140
|
Hello tommy.
|
|
141
|
-
» **tommy:** Which projects do I have?
|
|
141
|
+
-# » **tommy:** Which projects do I have?
|
|
142
142
|
-# ┣ shell _seq 1 2000; kimaki project list_
|
|
143
143
|
-# ┣ shell _kimaki send --channel 200000000000000100 --prompt 'Voice call note' --notify-on…_
|
|
144
|
-
-#
|
|
144
|
+
-# session started in <#thread>
|
|
145
145
|
-# ⨯ post\\_message _text must not be empty_
|
|
146
146
|
You have one project."
|
|
147
147
|
`)
|
|
@@ -205,12 +205,12 @@ test('chat messages reach the backend, and end_call leaves after the goodbye whi
|
|
|
205
205
|
expect(botPackets() - packetsBefore).toBeGreaterThanOrEqual(30)
|
|
206
206
|
expect(live.backendInputs.at(-1)).toBe(`<chat-message from="tommy" user-id="${TEST_USER_ID}">\nplease hang up\n</chat-message>`)
|
|
207
207
|
expect(await chatSince(before)).toMatchInlineSnapshot(`
|
|
208
|
-
"TestBot: -#
|
|
208
|
+
"TestBot: -# voice call started ⋅ gpt-live ⋅ cove
|
|
209
209
|
TestBot: Hi again.
|
|
210
210
|
tommy: please hang up
|
|
211
211
|
TestBot: -# ┣ end\\_call
|
|
212
212
|
TestBot: Bye.
|
|
213
|
-
TestBot: -#
|
|
213
|
+
TestBot: -# voice call ended: the assistant hung up"
|
|
214
214
|
`)
|
|
215
215
|
await twin.discord.channel(voiceChannelId).user(TEST_USER_ID).leaveVoice()
|
|
216
216
|
})
|
|
@@ -244,15 +244,15 @@ test('finished threads of any project are announced in the call; the model answe
|
|
|
244
244
|
live.replies.length = 0
|
|
245
245
|
|
|
246
246
|
expect(await chatSince(before)).toMatchInlineSnapshot(`
|
|
247
|
-
"TestBot: -#
|
|
247
|
+
"TestBot: -# voice call started ⋅ gpt-live ⋅ cove
|
|
248
248
|
TestBot: Hi.
|
|
249
249
|
tommy: start a thread
|
|
250
250
|
TestBot: -# ┣ shell _kimaki send --channel <id> --prompt 'call-thread-marker' --user '…_
|
|
251
|
-
TestBot: -#
|
|
251
|
+
TestBot: -# session started in <#<id>>
|
|
252
252
|
TestBot: Started.
|
|
253
|
-
TestBot: -#
|
|
253
|
+
TestBot: -# thread finished: call-thread-marker ⋅ https://discord.com/channels/<id>/<id> ⋅ <ses>
|
|
254
254
|
TestBot: Your thread finished.
|
|
255
|
-
TestBot: -#
|
|
255
|
+
TestBot: -# thread finished: other-thread-marker ⋅ https://discord.com/channels/<id>/<id> ⋅ <ses>"
|
|
256
256
|
`)
|
|
257
257
|
const notices = live.appends.filter((append) => append.text.startsWith('<system>Thread finished'))
|
|
258
258
|
expect(notices.map((notice) => ({ ...notice, text: notice.text.replace(/\d{17,20}/g, '<id>').replace(/ses_\w+/g, '<ses>') }))).toMatchInlineSnapshot(`
|
package/src/voice-call.ts
CHANGED
|
@@ -330,7 +330,7 @@ export function createVoiceCalls({ opencodeConfigDir }: { opencodeConfigDir: str
|
|
|
330
330
|
|
|
331
331
|
// Bot lines in the text chat of the voice channel, through the same FIFO as session output.
|
|
332
332
|
function post(bot: Bot, { channelId, text, markdown = false }: { channelId: string; text: string; markdown?: boolean }) {
|
|
333
|
-
bot.effects.run(channelId, [markdown ? { type: 'markdown', text, blankLineBefore: false } : { type: 'send', text }])
|
|
333
|
+
bot.effects.run(channelId, [markdown ? { type: 'markdown', text, blankLineBefore: false, replyTo: null } : { type: 'send', text }])
|
|
334
334
|
}
|
|
335
335
|
|
|
336
336
|
// Errors during a call go to the log and to the text chat of the voice channel.
|
|
@@ -505,7 +505,7 @@ export function createVoiceCalls({ opencodeConfigDir }: { opencodeConfigDir: str
|
|
|
505
505
|
sent.push(threadId)
|
|
506
506
|
current.started.add(threadId)
|
|
507
507
|
if (waits) current.waiting.add(threadId)
|
|
508
|
-
post(bot, { channelId: current.channelId, text: asSubtext(
|
|
508
|
+
post(bot, { channelId: current.channelId, text: asSubtext(`${started} <#${threadId}>`) })
|
|
509
509
|
}
|
|
510
510
|
: undefined
|
|
511
511
|
const run = runShell(bot, { call: current, command: input.command, timeoutMs, onLine }).then((result) => {
|
|
@@ -668,8 +668,8 @@ export function createVoiceCalls({ opencodeConfigDir }: { opencodeConfigDir: str
|
|
|
668
668
|
}
|
|
669
669
|
listen(bot, call)
|
|
670
670
|
logger.info(`voice call started in ${channelId}, voice ${settings.voice}`)
|
|
671
|
-
post(bot, { channelId, text: `-#
|
|
672
|
-
if (settings.voiceNotice) post(bot, { channelId, text: asSubtext(
|
|
671
|
+
post(bot, { channelId, text: `-# voice call started ⋅ gpt-live ⋅ ${settings.voice}` })
|
|
672
|
+
if (settings.voiceNotice) post(bot, { channelId, text: asSubtext(settings.voiceNotice) })
|
|
673
673
|
live.append({ text: 'The call started. Greet the users by name in one short sentence.', channel: 'speakable' })
|
|
674
674
|
}
|
|
675
675
|
|
|
@@ -684,7 +684,7 @@ export function createVoiceCalls({ opencodeConfigDir }: { opencodeConfigDir: str
|
|
|
684
684
|
call.speaker.stop()
|
|
685
685
|
if (call.connection.state.status !== VoiceConnectionStatus.Destroyed) call.connection.destroy()
|
|
686
686
|
logger.info(`voice call ended in ${call.channelId}${reason ? `: ${reason}` : ''}`)
|
|
687
|
-
post(bot, { channelId: call.channelId, text: `-#
|
|
687
|
+
post(bot, { channelId: call.channelId, text: `-# voice call ended${reason ? `: ${reason}` : ''}` })
|
|
688
688
|
}
|
|
689
689
|
|
|
690
690
|
async function onVoiceState(bot: Bot, { before, after }: { before: VoiceState; after: VoiceState }) {
|
|
@@ -727,7 +727,7 @@ export function createVoiceCalls({ opencodeConfigDir }: { opencodeConfigDir: str
|
|
|
727
727
|
if (!call || call.ending) return
|
|
728
728
|
const url = `https://discord.com/channels/${thread.guildId}/${threadId}`
|
|
729
729
|
const state = error === null ? 'finished' : 'failed'
|
|
730
|
-
post(bot, { channelId: call.channelId, text: asSubtext(
|
|
730
|
+
post(bot, { channelId: call.channelId, text: asSubtext(`thread ${state}: ${thread.name} ⋅ ${url} ⋅ ${sessionId}`) })
|
|
731
731
|
const failure = error === null ? '' : ` Error: ${error}`
|
|
732
732
|
const notice = `<system>Thread ${state}: "${thread.name}". Thread ID ${threadId}, session ID ${sessionId}, URL ${url}.${failure}</system>`
|
|
733
733
|
// Answer only for threads started in this call. A pending kimaki send --wait brings the transcript next.
|