kimaki 1.3.0 → 1.4.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/dist/bot.d.ts +3 -5
- package/dist/bot.js +3 -9
- package/dist/bot.js.map +1 -1
- 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 +73 -1
- package/dist/builtin-skills.test.js.map +1 -1
- package/dist/cli/bot.d.ts +1 -1
- package/dist/cli/bot.js +8 -4
- package/dist/cli/bot.js.map +1 -1
- package/dist/cli-lock.e2e.test.js +1 -1
- package/dist/event-loop.d.ts +1 -2
- package/dist/event-loop.js +2 -2
- package/dist/event-loop.js.map +1 -1
- package/dist/main.d.ts +2 -1
- package/dist/main.js +2 -2
- package/dist/main.js.map +1 -1
- package/dist/markdown/markdown.test.js +10 -0
- package/dist/markdown/markdown.test.js.map +1 -1
- package/dist/markdown/render-markdown.js +3 -1
- package/dist/markdown/render-markdown.js.map +1 -1
- package/dist/plugin/index.js +17 -44
- package/dist/plugin/index.js.map +1 -1
- package/dist/session-events.js +9 -5
- package/dist/session-events.js.map +1 -1
- package/dist/sessions.js +4 -3
- package/dist/sessions.js.map +1 -1
- package/dist/system-prompt.d.ts +3 -15
- package/dist/system-prompt.js +52 -77
- package/dist/system-prompt.js.map +1 -1
- package/dist/test/harness.d.ts +1 -2
- package/dist/test/harness.js +1 -2
- package/dist/test/harness.js.map +1 -1
- package/dist/test/replay.js +0 -1
- package/dist/test/replay.js.map +1 -1
- package/dist/thread-reducer.d.ts +0 -3
- package/dist/thread-reducer.fixtures.test.js +2 -2
- package/dist/thread-reducer.fixtures.test.js.map +1 -1
- package/dist/thread-reducer.js +4 -32
- package/dist/thread-reducer.js.map +1 -1
- package/dist/thread-reducer.test.js +0 -31
- 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/package.json +4 -4
- package/skills/kimaki-tunnel/SKILL.md +3 -2
- package/src/bot.ts +7 -14
- package/src/builtin-skills.test.ts +74 -1
- package/src/builtin-skills.ts +33 -0
- package/src/cli/bot.ts +7 -5
- package/src/cli-lock.e2e.test.ts +1 -1
- package/src/event-loop.ts +1 -4
- package/src/main.ts +5 -4
- package/src/markdown/markdown.test.ts +11 -0
- package/src/markdown/render-markdown.ts +3 -1
- package/src/plugin/index.ts +18 -44
- package/src/session-events.ts +8 -4
- package/src/sessions.ts +3 -3
- package/src/system-prompt.ts +56 -80
- package/src/test/harness.ts +0 -3
- package/src/test/replay.ts +0 -1
- package/src/thread-reducer.fixtures.test.ts +2 -2
- package/src/thread-reducer.test.ts +0 -31
- package/src/thread-reducer.ts +4 -36
- package/src/tunnel.test.ts +81 -5
- package/src/tunnel.ts +124 -25
- package/src/message-tool.e2e.test.ts +0 -45
package/src/system-prompt.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
import dedent from 'string-dedent'
|
|
6
6
|
import { z } from 'zod'
|
|
7
7
|
|
|
8
|
+
import type { PromptSection } from './builtin-skills.ts'
|
|
9
|
+
|
|
8
10
|
export const INSTRUCTION_KEY = 'kimaki'
|
|
9
11
|
|
|
10
12
|
export type InstructionAgent = { name: string; description: string }
|
|
@@ -15,8 +17,8 @@ export type ScheduledRun = { id: number; cronExpr: string | null; timezone: stri
|
|
|
15
17
|
|
|
16
18
|
const PARENT_SESSION_LINE = 'Your parent OpenCode session ID is: '
|
|
17
19
|
|
|
18
|
-
// Every system-level instruction of Kimaki is built in this file.
|
|
19
|
-
//
|
|
20
|
+
// Every system-level instruction of Kimaki is built in this file. Parts are
|
|
21
|
+
// joined in order; a tagged part is static and `--disable-prompt <tag>` drops it.
|
|
20
22
|
export function baseInstructions({
|
|
21
23
|
sessionId,
|
|
22
24
|
threadId,
|
|
@@ -28,6 +30,7 @@ export function baseInstructions({
|
|
|
28
30
|
agents,
|
|
29
31
|
parentSessionId,
|
|
30
32
|
scheduledTask = null,
|
|
33
|
+
disabled = [],
|
|
31
34
|
}: {
|
|
32
35
|
sessionId: string
|
|
33
36
|
threadId: string
|
|
@@ -41,13 +44,15 @@ export function baseInstructions({
|
|
|
41
44
|
// Only from an explicit `kimaki send --parent-session`.
|
|
42
45
|
parentSessionId: string | null
|
|
43
46
|
scheduledTask?: ScheduledRun | null
|
|
47
|
+
// `kimaki --disable-prompt`: tagged static parts to leave out.
|
|
48
|
+
disabled?: readonly PromptSection[]
|
|
44
49
|
}): string {
|
|
45
50
|
const userArg = ` --user '${userId}'`
|
|
46
51
|
const parentArg = ` --parent-session ${sessionId}`
|
|
47
52
|
const topic = channelTopic?.trim()
|
|
48
53
|
const agentList = agents.map((agent) => `- \`${agent.name}\`${agent.description ? `: ${agent.description}` : ''}`).join('\n')
|
|
49
|
-
const
|
|
50
|
-
dedent`
|
|
54
|
+
const parts: Array<[PromptSection | null, string]> = [
|
|
55
|
+
[null, dedent`
|
|
51
56
|
The user is reading your messages from inside Discord, via kimaki.dev.
|
|
52
57
|
|
|
53
58
|
## Discord output
|
|
@@ -76,8 +81,8 @@ export function baseInstructions({
|
|
|
76
81
|
\`description\` is shown in Discord when the shell command is longer than 50 characters.
|
|
77
82
|
\`hasSideEffect\` only decides if Discord shows the shell call. Set it to \`true\` for every command the user should be aware of, for example commands that change state or that the user asked you to run (the user says "run sleep 100", so \`sleep 100\` gets \`true\`). Set \`false\` only for routine commands the user does not need to see.
|
|
78
83
|
The shell environment provides the \`kimaki\` command and OPENCODE_SESSION_ID. Do not pass stale IDs copied from another session.
|
|
79
|
-
|
|
80
|
-
dedent`
|
|
84
|
+
`],
|
|
85
|
+
[null, dedent`
|
|
81
86
|
Discord channel ID: ${channelId}
|
|
82
87
|
Discord thread ID: ${threadId}
|
|
83
88
|
Discord guild ID: ${guildId}
|
|
@@ -94,8 +99,8 @@ export function baseInstructions({
|
|
|
94
99
|
\`\`\`
|
|
95
100
|
|
|
96
101
|
Links have the form \`https://discord.com/channels/<guild_id>/<thread_id>\`. For changes made by another session, use that session's link (see "who edited a file").
|
|
97
|
-
|
|
98
|
-
dedent`
|
|
102
|
+
`],
|
|
103
|
+
['discord-permissions', dedent`
|
|
99
104
|
## permissions
|
|
100
105
|
|
|
101
106
|
Only users with these Discord permissions can send messages to the bot:
|
|
@@ -105,11 +110,13 @@ export function baseInstructions({
|
|
|
105
110
|
- "Kimaki" role (case-insensitive)
|
|
106
111
|
|
|
107
112
|
Other Discord bots are ignored by default. To allow another bot to trigger sessions (for multi-agent orchestration), assign it the "Kimaki" role.
|
|
108
|
-
|
|
113
|
+
`],
|
|
114
|
+
['kimaki-debugging', dedent`
|
|
109
115
|
## debugging kimaki issues
|
|
110
116
|
|
|
111
117
|
When sessions do not respond, the bot shows errors, or you need the Kimaki or OpenCode logs, load the \`kimaki-debug\` skill (logs, profiling, \`kimaki login\` for provider credentials) and read the logs before guessing. Do not restart the bot unless the user explicitly asks. ALWAYS read https://kimaki.dev/docs/guides/report-bugs before submitting any issue to Kimaki. Never open a pull request on remorses/kimaki unless remorses asked for one in a comment on the issue.
|
|
112
|
-
|
|
118
|
+
`],
|
|
119
|
+
[null, dedent`
|
|
113
120
|
## uploading files to discord
|
|
114
121
|
|
|
115
122
|
To upload files to the Discord thread (images, screenshots, long files that would clutter the chat), run:
|
|
@@ -137,20 +144,23 @@ export function baseInstructions({
|
|
|
137
144
|
\`\`\`
|
|
138
145
|
|
|
139
146
|
See \`kimaki tts --help\` for options like voice, speed, and style instructions.
|
|
140
|
-
|
|
147
|
+
`],
|
|
148
|
+
['file-upload-requests', dedent`
|
|
141
149
|
## requesting files from the user
|
|
142
150
|
|
|
143
151
|
To ask the user to upload files from their device, write all visible text first, then run \`kimaki upload-request --prompt 'Send the files' --max-files 5\`. This shows a native file picker in Discord. Set the shell timeout to 600000 ms.
|
|
144
152
|
The request waits at most 6 minutes and returns the local paths (in the session folder under \`uploads/\`) or cancelled. A new user prompt cancels the request. Do not keep waiting after cancellation.
|
|
145
153
|
You MUST call it LAST, after ALL text.
|
|
146
|
-
|
|
154
|
+
`],
|
|
155
|
+
['action-buttons', dedent`
|
|
147
156
|
## action buttons
|
|
148
157
|
|
|
149
158
|
To show 1 to 3 buttons, write all visible text first, then run \`kimaki buttons --button 'Label'\`. Repeat \`--button\` for more. Prefer a single button whenever possible. Colors are white, blue, green, red: \`--button 'Label:green'\`.
|
|
150
159
|
A click sends "User clicked: Label" to this session as a new prompt. Labels have at most 80 chars.
|
|
151
160
|
You MUST call \`kimaki buttons\` LAST, after ALL text. Never call it in a turn that has no text before it. The text must explain the choice. Labels alone are not an explanation.
|
|
152
161
|
The user does not see tool calls, command outputs, or subagent and task results. Before the buttons, concisely explain what was done in the session and any findings the choice depends on, then what each button does.
|
|
153
|
-
|
|
162
|
+
`],
|
|
163
|
+
['session-sleep', dedent`
|
|
154
164
|
## sleeping the session
|
|
155
165
|
|
|
156
166
|
Use \`kimaki sleep\` to pause this session for hours or days, then continue when the time is reached. The wake is stored in SQLite and survives bot restarts.
|
|
@@ -161,8 +171,9 @@ export function baseInstructions({
|
|
|
161
171
|
Wake is a later message that starts with \`Woke after sleeping until\`. Only then continue the wait reason.
|
|
162
172
|
To monitor something slow (email replies, PR reviews, long CI), use long waits: 2h or more, up to 1d. Never poll every few minutes. Prefer \`--until\` at a natural check time, for example the next morning.
|
|
163
173
|
On a wake where nothing changed, do not post a status report. Write one short line, then sleep again with a longer wait (double it, up to 1d).
|
|
164
|
-
|
|
165
|
-
|
|
174
|
+
`],
|
|
175
|
+
[null, scheduledTask ? scheduledTaskSection(scheduledTask) : ''],
|
|
176
|
+
[null, dedent`
|
|
166
177
|
## archiving the current thread
|
|
167
178
|
|
|
168
179
|
To archive the current Discord thread (hide it from sidebar) without stopping the session, run:
|
|
@@ -170,7 +181,8 @@ export function baseInstructions({
|
|
|
170
181
|
kimaki session archive ${threadId}
|
|
171
182
|
|
|
172
183
|
Only do this when the user explicitly asks to close or archive the thread, and only after your final message.
|
|
173
|
-
|
|
184
|
+
`],
|
|
185
|
+
['session-abort', dedent`
|
|
174
186
|
## aborting a session
|
|
175
187
|
|
|
176
188
|
If you made a mistake with \`kimaki send\` (wrong prompt, wrong channel, mangled heredoc), abort the session immediately using the session ID printed in the output:
|
|
@@ -178,7 +190,8 @@ export function baseInstructions({
|
|
|
178
190
|
kimaki session abort <session_id>
|
|
179
191
|
|
|
180
192
|
This stops the AI from processing but keeps the thread visible in Discord. Different from \`kimaki session archive\` which hides the thread.
|
|
181
|
-
|
|
193
|
+
`],
|
|
194
|
+
[null, dedent`
|
|
182
195
|
## updating the session title
|
|
183
196
|
|
|
184
197
|
Skip the first turn. OpenCode already auto-generates the title from the first message.
|
|
@@ -201,8 +214,8 @@ export function baseInstructions({
|
|
|
201
214
|
kimaki user list --guild ${guildId} --query "username"
|
|
202
215
|
|
|
203
216
|
This returns user IDs you can use for Discord mentions. It can fail when Server Members Intent is disabled, so prefer IDs from existing Discord metadata or raw mentions when possible.
|
|
204
|
-
|
|
205
|
-
dedent`
|
|
217
|
+
`],
|
|
218
|
+
[null, dedent`
|
|
206
219
|
## starting new sessions from CLI
|
|
207
220
|
|
|
208
221
|
Start a new thread/session in this channel with:
|
|
@@ -356,7 +369,8 @@ export function baseInstructions({
|
|
|
356
369
|
Thread reminders: when the user says "remind me about this in 2 hours", schedule a send to this thread. \`--notify-only\` does not work with \`--thread\`; the scheduled prompt always runs in that thread's session:
|
|
357
370
|
|
|
358
371
|
kimaki send --thread ${threadId} --prompt 'Reminder: you asked to be reminded about this thread.' --send-at '<future_UTC_time>' --agent <current_agent>${userArg}
|
|
359
|
-
|
|
372
|
+
`],
|
|
373
|
+
[null, dedent`
|
|
360
374
|
## reading other sessions
|
|
361
375
|
|
|
362
376
|
\`\`\`bash
|
|
@@ -369,7 +383,7 @@ export function baseInstructions({
|
|
|
369
383
|
|
|
370
384
|
Each row shows the session ID, status (\`busy\`, \`waiting\` for input, or \`idle\`), the title, and \`tokens: N\`. Titles prefixed with \`btw:\` are side sessions that answer a related user question in parallel. They are not duplicate sessions of the main task.
|
|
371
385
|
|
|
372
|
-
To search past sessions, search the transcripts. Kimaki writes one markdown file per session to \`${dataDir}/transcripts/<session id>.md\` when a run ends (user messages, assistant text
|
|
386
|
+
To search past sessions, search the transcripts. Kimaki writes one markdown file per session to \`${dataDir}/transcripts/<session id>.md\` when a run ends (user messages, assistant text, thinking, tool inputs up to 5000 chars and compaction summaries; no tool outputs). The first lines are \`# <title> (<session id>)\` and \`directory: <folder>\`; messages start with \`## user <time>\` or \`## assistant <time> (<agent>, <model>)\`. Use rg (or \`grep -r\` when rg is missing):
|
|
373
387
|
|
|
374
388
|
\`\`\`bash
|
|
375
389
|
rg -il 'auth timeout' ${dataDir}/transcripts # sessions that mention it; the file name is the session ID
|
|
@@ -418,7 +432,8 @@ export function baseInstructions({
|
|
|
418
432
|
Output is newest first. Each row has the **session ID** (\`ses_xxx\`), the **title** (Discord thread name, so you can tell what that session was doing), and **time ago** (when it last edited the file).
|
|
419
433
|
|
|
420
434
|
Use this before committing a file this session did not edit. Get the original session's link with \`kimaki session url <session_id>\` and put it as the last line of the commit message: \`Discord: https://discord.com/channels/<guild_id>/<thread_id>\`. If that session has no Kimaki thread, use \`Session: ses_xxx\` instead. If this session edited the file, use this thread's link. If files come from different sessions, split the commit by session. Do not attribute another session's edits to this one.
|
|
421
|
-
|
|
435
|
+
`],
|
|
436
|
+
['cross-project-commands', dedent`
|
|
422
437
|
## cross-project commands
|
|
423
438
|
|
|
424
439
|
When the user references another project by name, run \`kimaki project list\` to find its directory and channel ID, then read files, search code, or run commands directly in that directory. If the project is not listed, register its root with \`kimaki project add /path/to/repo\` (this creates a Discord channel). Never add subfolders of an existing project.
|
|
@@ -441,7 +456,8 @@ export function baseInstructions({
|
|
|
441
456
|
kimaki send --channel <channel_id> --prompt 'Plan how to update the API client to v2' --agent <current_agent>
|
|
442
457
|
kimaki send --project /path/to/other-repo --prompt 'Plan how to bump version to 1.2.0' --agent <current_agent>
|
|
443
458
|
\`\`\`
|
|
444
|
-
|
|
459
|
+
`],
|
|
460
|
+
['waiting-sessions', dedent`
|
|
445
461
|
## waiting for a session to finish
|
|
446
462
|
|
|
447
463
|
\`--wait\` blocks until a session completes and prints its full conversation to stdout. Use it when you need another session's result before continuing: fixing a bug in another project first, or chaining sessions where the next depends on the previous output. When the user asks you to wait for an existing session, run \`kimaki session wait <session_id>\` yourself via the shell tool and continue from the printed markdown. Do not tell the user to run it.
|
|
@@ -461,12 +477,8 @@ export function baseInstructions({
|
|
|
461
477
|
\`\`\`
|
|
462
478
|
|
|
463
479
|
\`session list --active\` exits 0 while it finds active sessions, 1 when none remain, and 64 on errors. The loop stops only on 1, so an error never looks like "no active sessions". \`--active\` never lists the calling session (\`OPENCODE_SESSION_ID\`), so the loop does not wait for itself. Sessions can start again after the loop ends, so run the check again right before each commit. \`session wait\` returns once the model finishes responding, or when the session pauses to show the user a question (it does not finish on its own until answered).
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
When pulling submodules and they jump to a new commit, commit that submodule pointer update right away before doing other work. Otherwise critique diffs later will include the noisy submodule jump along with the real changes.
|
|
468
|
-
`,
|
|
469
|
-
dedent`
|
|
480
|
+
`],
|
|
481
|
+
['critique-diffs', dedent`
|
|
470
482
|
## showing diffs
|
|
471
483
|
|
|
472
484
|
The user cannot see tool output. Share diffs as critique web URLs, never raw \`git diff\` output:
|
|
@@ -495,14 +507,13 @@ export function baseInstructions({
|
|
|
495
507
|
|
|
496
508
|
If the changes are already committed (only commit when the user asks), show one URL per commit with \`bunx critique --commit <hash> --web\`, running the critique calls in parallel.
|
|
497
509
|
|
|
510
|
+
When pulling submodules and they jump to a new commit, commit that submodule pointer update right away before doing other work. Otherwise critique diffs later will include the noisy submodule jump along with the real changes.
|
|
511
|
+
|
|
498
512
|
Users can leave line comments on a diff page (Agentation widget, bottom right). When they say they did, read them with \`curl https://critique.work/v/<id>/annotations\` (or WebFetch). It returns markdown with file, line, and comment text.
|
|
499
513
|
|
|
500
514
|
critique is open source (MIT, https://github.com/remorses/critique). Diff URLs are unique, unguessable, not indexed, and ephemeral. If the user is worried about uploading code, tell them this.
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
Localhost URLs are useless from Discord. When the user should open a local dev server in a browser, load the \`kimaki-tunnel\` skill: it wraps the server in \`kimaki tunnel\` to get a public URL.
|
|
505
|
-
|
|
515
|
+
`],
|
|
516
|
+
['markdown-formatting', dedent`
|
|
506
517
|
## markdown formatting
|
|
507
518
|
|
|
508
519
|
Format responses in **Claude-style markdown** - structured, scannable, never walls of text. Use:
|
|
@@ -519,7 +530,8 @@ export function baseInstructions({
|
|
|
519
530
|
Discord supports: headings, bold, italic, strikethrough, code blocks, inline code, quotes, lists, and links.
|
|
520
531
|
|
|
521
532
|
NEVER wrap URLs in inline code or code blocks - this breaks clickability in Discord. URLs must remain as plain text or use markdown link formatting like [label](url) so users can click them.
|
|
522
|
-
|
|
533
|
+
`],
|
|
534
|
+
['discord-callouts', dedent`
|
|
523
535
|
## Callouts in Kimaki Discord
|
|
524
536
|
|
|
525
537
|
Use \`<callout>\` HTML blocks for important notices in Discord. Do **not** use GitHub callout syntax like \`> [!WARNING]\`, because Kimaki renders \`<callout>\` natively.
|
|
@@ -553,15 +565,18 @@ export function baseInstructions({
|
|
|
553
565
|
- action-required notes, breaking caveats, or important limitations, use **purple** like \`#8b5cf6\`
|
|
554
566
|
|
|
555
567
|
Do not wrap the whole response in callouts. Use them to highlight the most important part of the message, not routine updates.
|
|
556
|
-
|
|
568
|
+
`],
|
|
569
|
+
['search-result-urls', dedent`
|
|
557
570
|
## URLs in search results
|
|
558
571
|
|
|
559
572
|
When performing web searches, code searches, or any lookup that returns URLs (GitHub repos, docs, Stack Overflow, npm packages, etc.), ALWAYS include the URLs in your response so the user can click them. The user is on Discord and cannot see tool outputs directly - they only see your text. If you found a relevant link, show it. Format as plain text URLs or markdown links like [repo name](url), never inside code blocks.
|
|
560
|
-
|
|
573
|
+
`],
|
|
574
|
+
['ascii-diagrams', dedent`
|
|
561
575
|
## diagrams
|
|
562
576
|
|
|
563
577
|
Make heavy use of diagrams to explain architecture, flows, and relationships. Create diagrams using ASCII art inside code blocks. Prefer diagrams over lengthy text explanations whenever possible. Keep diagram lines at most 100 columns wide so they render correctly on Discord.
|
|
564
|
-
|
|
578
|
+
`],
|
|
579
|
+
['action-buttons', dedent`
|
|
565
580
|
## ending conversations with options
|
|
566
581
|
|
|
567
582
|
The \`question\` tool is disabled in Kimaki sessions. To offer choices, use \`kimaki buttons\` instead.
|
|
@@ -577,10 +592,10 @@ export function baseInstructions({
|
|
|
577
592
|
- After completing edits: offer "Commit changes?"
|
|
578
593
|
- If a plan has multiple strategy of implementation show these as options
|
|
579
594
|
- After a genuinely ambiguous request where you cannot infer intent: offer the different approaches
|
|
580
|
-
|
|
581
|
-
|
|
595
|
+
`],
|
|
596
|
+
[null, topic ? `<channel-topic>\n${topic}\n</channel-topic>` : ''],
|
|
582
597
|
]
|
|
583
|
-
return
|
|
598
|
+
return parts.filter(([tag, text]) => text !== '' && !(tag && disabled.includes(tag))).map(([, text]) => text).join('\n\n')
|
|
584
599
|
}
|
|
585
600
|
|
|
586
601
|
function scheduledTaskSection(task: ScheduledRun): string {
|
|
@@ -588,7 +603,6 @@ function scheduledTaskSection(task: ScheduledRun): string {
|
|
|
588
603
|
? `Schedule: cron \`${task.cronExpr}\` in ${task.timezone || 'UTC'}. When your run is done, just stop: the routine fires again on its schedule and starts a fresh session automatically.`
|
|
589
604
|
: 'This routine runs once and does not repeat. When your run is done, just stop.'
|
|
590
605
|
return dedent`
|
|
591
|
-
|
|
592
606
|
## routine session
|
|
593
607
|
|
|
594
608
|
This session was started automatically by kimaki routine #${task.id}.
|
|
@@ -645,44 +659,6 @@ export function turnContext(metadata: Readonly<Record<string, unknown>> | undefi
|
|
|
645
659
|
return `${files}<discord-user name="${escapeAttribute(username)}" user-id="${userId}" message-id="${messageId}" thread-id="${threadId}" thread-name="${escapeAttribute(threadName)}" />`
|
|
646
660
|
}
|
|
647
661
|
|
|
648
|
-
// `kimaki --message-tool`: root sessions get this tool. Discord shows only its text;
|
|
649
|
-
// plain text parts stay visible in the OpenCode TUI and other clients.
|
|
650
|
-
// Wording adapted from Grok Bot's SendMessage tool.
|
|
651
|
-
export const MESSAGE_TOOL = 'send_message'
|
|
652
|
-
|
|
653
|
-
export const MESSAGE_TOOL_DESCRIPTION = dedent`
|
|
654
|
-
Send a concise reply to the user in the Discord thread. Discord shows only the text of send_message calls, not your plain text. Keep writing your normal plain text replies: they stay visible in the OpenCode terminal UI. Then repeat the part the Discord user needs as a short send_message, also for short replies like "Done" or "Yes, it does".
|
|
655
|
-
|
|
656
|
-
If you end a turn where the user waits on you without calling send_message, Discord shows silence.
|
|
657
|
-
|
|
658
|
-
Be very concise. Send meaningful beats only: a real result, a decision, a blocker, a change of plan. Do not narrate routine steps, retries or minor snags. Prefer fewer, higher-signal messages over a play-by-play, but never vanish into a long silent run.
|
|
659
|
-
|
|
660
|
-
An opening acknowledgement does not deliver the result. If you ran something for the user, send the actual result before you end the turn.
|
|
661
|
-
|
|
662
|
-
\`text\` is Discord markdown, shown like a normal message: headings, lists, code blocks, tables and <callout> blocks work. Send files with \`kimaki upload-to-discord\`, never through this tool. After your final send_message, end the turn.
|
|
663
|
-
`
|
|
664
|
-
|
|
665
|
-
export const MESSAGE_TOOL_INPUT = {
|
|
666
|
-
type: 'object',
|
|
667
|
-
properties: { text: { type: 'string', description: 'Markdown shown to the user in Discord. Keep it short.' } },
|
|
668
|
-
required: ['text'],
|
|
669
|
-
additionalProperties: false,
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
// Same text for the whole session (the prompt cache keeps hitting). It overrides
|
|
673
|
-
// the "Discord output" rules of the instruction entry, which assume Discord shows text parts.
|
|
674
|
-
export const MESSAGE_MODE_INSTRUCTIONS = dedent`
|
|
675
|
-
## Discord shows only send_message
|
|
676
|
-
|
|
677
|
-
This overrides every rule above about what Discord shows: Discord does NOT show your plain text. Only the \`text\` of \`send_message\` tool calls reaches the Discord thread. The same session can also be open in the OpenCode terminal UI, which shows your plain text normally.
|
|
678
|
-
|
|
679
|
-
- Write your plain text replies as usual, in full. They are for the terminal UI.
|
|
680
|
-
- Then repeat a concise version as send_message, in Discord markdown: every reply, question and final answer, even one word of small talk.
|
|
681
|
-
- Where these instructions say "write the text first" before \`kimaki buttons\`, \`kimaki upload-request\` or \`kimaki sleep\`, call send_message with that text first.
|
|
682
|
-
- Never end a turn with a reply only in plain text.
|
|
683
|
-
- Never mention send_message, tool names or this rule to the user.
|
|
684
|
-
`
|
|
685
|
-
|
|
686
662
|
export type VoiceCallUser = { id: string; username: string }
|
|
687
663
|
export type VoiceCallProject = { channelId: string; channelName: string; directory: string }
|
|
688
664
|
// skill.list entry of OpenCode's global location; path is the absolute SKILL.md.
|
package/src/test/harness.ts
CHANGED
|
@@ -499,7 +499,6 @@ export async function startTestBot({
|
|
|
499
499
|
analytics,
|
|
500
500
|
clock,
|
|
501
501
|
schedulerIntervalMs,
|
|
502
|
-
messageTool,
|
|
503
502
|
}: {
|
|
504
503
|
dataDir: string
|
|
505
504
|
twin: TestTwin
|
|
@@ -520,7 +519,6 @@ export async function startTestBot({
|
|
|
520
519
|
// Scheduling tests pass manualClock() and null, then call bot.scheduler.runDueTasks().
|
|
521
520
|
clock?: Clock
|
|
522
521
|
schedulerIntervalMs?: number | null
|
|
523
|
-
messageTool?: boolean
|
|
524
522
|
}): Promise<BotHandle> {
|
|
525
523
|
// What credential resolution saves in production; `kimaki project add` reads it.
|
|
526
524
|
const credentials: Credentials = twin.discord.botToken.includes(':')
|
|
@@ -549,7 +547,6 @@ export async function startTestBot({
|
|
|
549
547
|
analytics: analytics ?? disabledAnalytics,
|
|
550
548
|
...(clock && { clock }),
|
|
551
549
|
...(schedulerIntervalMs !== undefined && { schedulerIntervalMs }),
|
|
552
|
-
...(messageTool && { messageTool }),
|
|
553
550
|
...(realtimeBaseUrls && { realtimeBaseUrls }),
|
|
554
551
|
transcriptionBaseUrls: {
|
|
555
552
|
...(geminiBaseUrl && { gemini: geminiBaseUrl }),
|
package/src/test/replay.ts
CHANGED
|
@@ -31,7 +31,7 @@ test('tools: names from tool.input.started, failed tool lines, read-only tools h
|
|
|
31
31
|
test('tools at text verbosity: text, edits and errors only', () => {
|
|
32
32
|
const { effects } = replay({
|
|
33
33
|
events: loadFixture('tools.events.jsonl'),
|
|
34
|
-
prefs: { verbosity: 'text', contextLimits: {}
|
|
34
|
+
prefs: { verbosity: 'text', contextLimits: {} },
|
|
35
35
|
})
|
|
36
36
|
expect(effectLines(effects)).toMatchInlineSnapshot(`
|
|
37
37
|
[
|
|
@@ -324,7 +324,7 @@ test('permission: two requests, the rejected one fails its tool call', () => {
|
|
|
324
324
|
test('shell-background: background shell line even without side effects, finished line when its job ends', () => {
|
|
325
325
|
const events = loadFixture('shell-background.events.jsonl')
|
|
326
326
|
const view = emptyView({ sessionId: 'ses_ee56d6caeffech0rMI5NTnkky1', channelId: 'channel', directory: '/project', isNew: false })
|
|
327
|
-
const { effects, view: end } = replay({ events, view, prefs: { verbosity: 'tools', contextLimits: {}
|
|
327
|
+
const { effects, view: end } = replay({ events, view, prefs: { verbosity: 'tools', contextLimits: {} } })
|
|
328
328
|
expect(effectLines(effects)).toMatchInlineSnapshot(`
|
|
329
329
|
[
|
|
330
330
|
"» **user:** start a task in background that runs sleep 100",
|
|
@@ -55,37 +55,6 @@ test('text-only turn: banner, text, typing, footer with context percent', () =>
|
|
|
55
55
|
expect(view.turn).toBe(null)
|
|
56
56
|
})
|
|
57
57
|
|
|
58
|
-
test('message mode: send_message shows as markdown, plain text only when the turn sent nothing', () => {
|
|
59
|
-
const turn = loadFixture('tools.events.jsonl').filter((event) => !event.type.startsWith('session.tool.'))
|
|
60
|
-
const prefs = { ...DEFAULT_PREFS, messageTool: true }
|
|
61
|
-
const textIndex = turn.findIndex((event) => event.type === 'session.text.ended')
|
|
62
|
-
const text = turn[textIndex]!
|
|
63
|
-
if (text.type !== 'session.text.ended') throw new Error('Expected text')
|
|
64
|
-
const call = { sessionID: text.data.sessionID, assistantMessageID: text.data.assistantMessageID, id: 'call_message' }
|
|
65
|
-
const durable = { aggregateID: text.data.sessionID, seq: 1, version: 1 as const }
|
|
66
|
-
const message: V2Event[] = [
|
|
67
|
-
{ id: 'evt_message_input', created: text.created, durable, type: 'session.tool.input.started', data: { ...call, name: 'send_message' } },
|
|
68
|
-
{ id: 'evt_message_called', created: text.created, durable, type: 'session.tool.called', data: { ...call, executed: false, input: { text: 'Visible **reply**' } } },
|
|
69
|
-
]
|
|
70
|
-
const sent = replay({ prefs, events: [...turn.slice(0, textIndex), ...message, ...turn.slice(textIndex)] })
|
|
71
|
-
const silent = replay({ prefs, events: turn })
|
|
72
|
-
const lines = (effects: typeof sent.effects) => effectLines(effects).filter((line) => !line.startsWith('»') && !line.startsWith('[typing'))
|
|
73
|
-
expect({ sent: lines(sent.effects), silent: lines(silent.effects) }).toMatchInlineSnapshot(`
|
|
74
|
-
{
|
|
75
|
-
"sent": [
|
|
76
|
-
"-# *using openai/gpt-6-luna low ⋅ build*",
|
|
77
|
-
"Visible **reply**",
|
|
78
|
-
"-# *project ⋅ main ⋅ 17s ⋅ 6% ⋅ gpt-6-luna*",
|
|
79
|
-
],
|
|
80
|
-
"silent": [
|
|
81
|
-
"-# *using openai/gpt-6-luna low ⋅ build*",
|
|
82
|
-
"Done. \`tmp-events/hello.txt\` contains \`hello world\`.",
|
|
83
|
-
"-# *project ⋅ main ⋅ 17s ⋅ 6% ⋅ gpt-6-luna*",
|
|
84
|
-
],
|
|
85
|
-
}
|
|
86
|
-
`)
|
|
87
|
-
})
|
|
88
|
-
|
|
89
58
|
test('kimaki sleep in a turn makes its footer silent, the next turn notifies again', () => {
|
|
90
59
|
const turn = loadFixture('tools.events.jsonl').filter((event) => !event.type.startsWith('session.tool.'))
|
|
91
60
|
const end = turn.findIndex((event) => event.type === 'session.execution.succeeded')
|
package/src/thread-reducer.ts
CHANGED
|
@@ -55,7 +55,6 @@ import {
|
|
|
55
55
|
import { closePermission, hydratePermissions, showPermission, STATUS, type PendingPermission } from './permissions.ts'
|
|
56
56
|
import { closeForm, formatAnswer, hydrateForms, showForm, withAnswer, type PendingForm } from './questions.ts'
|
|
57
57
|
import { cancelQueued, deliverQueued, enqueueInput, hydrateQueue, promoteQueued, queuedItems, type PendingInput } from './queue.ts'
|
|
58
|
-
import { MESSAGE_TOOL } from './system-prompt.ts'
|
|
59
58
|
|
|
60
59
|
export type Turn = {
|
|
61
60
|
startedAt: number
|
|
@@ -65,14 +64,6 @@ export type Turn = {
|
|
|
65
64
|
tokens: number
|
|
66
65
|
// `kimaki sleep` ran in this turn: the session wakes later, so the footer is silent.
|
|
67
66
|
slept: boolean
|
|
68
|
-
// Message mode (Prefs.messageTool): a send_message call was shown in this turn.
|
|
69
|
-
messaged: boolean
|
|
70
|
-
// Message mode: the last hidden text, shown at the end if the turn sent no message.
|
|
71
|
-
unsent: string | null
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function newTurn(at: number): Turn {
|
|
75
|
-
return { startedAt: at, model: null, agent: null, tokens: 0, slept: false, messaged: false, unsent: null }
|
|
76
67
|
}
|
|
77
68
|
|
|
78
69
|
export type Child = {
|
|
@@ -195,8 +186,6 @@ export type Prefs = {
|
|
|
195
186
|
verbosity: Verbosity
|
|
196
187
|
// "providerID/modelID" -> context window size in tokens.
|
|
197
188
|
contextLimits: Readonly<Record<string, number>>
|
|
198
|
-
// `kimaki --message-tool`: Discord hides root plain text (kept for the TUI), only send_message shows.
|
|
199
|
-
messageTool: boolean
|
|
200
189
|
}
|
|
201
190
|
|
|
202
191
|
const RETRY_NOTICE_INTERVAL_MS = 10_000
|
|
@@ -292,9 +281,6 @@ function contextPercent({ turn, prefs }: { turn: Draft<Turn>; prefs: Prefs }): n
|
|
|
292
281
|
|
|
293
282
|
// The footer closing the answer of `turn` at `at`.
|
|
294
283
|
function emitFooter({ draft, prefs, emit, turn, at, notify }: Context & { turn: Draft<Turn>; at: number; notify: boolean }) {
|
|
295
|
-
// Message mode: a model that never called send_message would leave the user in silence.
|
|
296
|
-
if (!turn.messaged && turn.unsent) emit({ type: 'markdown', text: turn.unsent, blankLineBefore: draft.lastKind === 'tool' })
|
|
297
|
-
turn.unsent = null
|
|
298
284
|
// No model step ran (a compaction-only execution): nothing to summarize.
|
|
299
285
|
if (!turn.model) return
|
|
300
286
|
// A child still runs: the answer comes later.
|
|
@@ -360,11 +346,11 @@ function applyRoot(context: Context & { event: V2Event }) {
|
|
|
360
346
|
emit({ type: 'send', text: asSubtext(`Working directory changed to ${event.data.location.directory}`) })
|
|
361
347
|
return
|
|
362
348
|
case 'session.execution.started':
|
|
363
|
-
draft.turn ??=
|
|
349
|
+
draft.turn ??= { startedAt: event.created, model: null, agent: null, tokens: 0, slept: false }
|
|
364
350
|
return
|
|
365
351
|
case 'session.step.started': {
|
|
366
352
|
const model = { providerID: event.data.model.providerID, id: event.data.model.id }
|
|
367
|
-
draft.turn ??=
|
|
353
|
+
draft.turn ??= { startedAt: event.created, model: null, agent: null, tokens: 0, slept: false }
|
|
368
354
|
draft.turn.model = model
|
|
369
355
|
draft.turn.agent = event.data.agent
|
|
370
356
|
if (draft.bannerPending) emit({ type: 'send', text: formatBanner({ model: event.data.model, agent: event.data.agent }) })
|
|
@@ -387,10 +373,6 @@ function applyRoot(context: Context & { event: V2Event }) {
|
|
|
387
373
|
case 'session.text.ended': {
|
|
388
374
|
const text = event.data.text.trim()
|
|
389
375
|
if (!text) return
|
|
390
|
-
if (prefs.messageTool && draft.turn) {
|
|
391
|
-
draft.turn.unsent = text
|
|
392
|
-
return
|
|
393
|
-
}
|
|
394
376
|
emit({ type: 'markdown', text, blankLineBefore: draft.lastKind === 'tool' })
|
|
395
377
|
draft.lastKind = 'text'
|
|
396
378
|
return
|
|
@@ -452,7 +434,6 @@ function applyRoot(context: Context & { event: V2Event }) {
|
|
|
452
434
|
emitFooter({ ...context, turn: draft.turn, at: event.created, notify: false })
|
|
453
435
|
draft.turn.startedAt = event.created
|
|
454
436
|
draft.turn.slept = false
|
|
455
|
-
draft.turn.messaged = false
|
|
456
437
|
draft.lastKind = null
|
|
457
438
|
}
|
|
458
439
|
return deliverQueued({ draft, emit, inboxID: event.data.inboxID })
|
|
@@ -513,15 +494,6 @@ function applyTool(context: Context & { event: V2Event; label: string | undefine
|
|
|
513
494
|
if (reused) Object.assign(reused, { ...call, agent: call.agent || reused.agent })
|
|
514
495
|
}
|
|
515
496
|
draft.tools[key] = tool
|
|
516
|
-
// The plugin offers send_message to root sessions only (MESSAGE_TOOL).
|
|
517
|
-
if (name === MESSAGE_TOOL && label === undefined) {
|
|
518
|
-
const text = stringInput(input, 'text').trim()
|
|
519
|
-
if (draft.turn) draft.turn.messaged = true
|
|
520
|
-
if (!text) return
|
|
521
|
-
context.emit({ type: 'markdown', text, blankLineBefore: draft.lastKind === 'tool' })
|
|
522
|
-
draft.lastKind = 'text'
|
|
523
|
-
return
|
|
524
|
-
}
|
|
525
497
|
if (render && isToolVisible({ name, input }, prefs.verbosity)) toolLine(context, formatToolLine({ name, input }, { label }))
|
|
526
498
|
return
|
|
527
499
|
}
|
|
@@ -641,7 +613,7 @@ function applyKimaki({ draft, event, prefs, emit }: Context & { event: KimakiEve
|
|
|
641
613
|
const active = new Set(event.activeSessionIds)
|
|
642
614
|
for (const [id, child] of Object.entries(draft.children)) child.running = active.has(id)
|
|
643
615
|
if (!active.has(draft.sessionId)) draft.turn = null
|
|
644
|
-
else draft.turn ??=
|
|
616
|
+
else draft.turn ??= { startedAt: event.at, model: null, agent: null, tokens: 0, slept: false }
|
|
645
617
|
// Nothing runs: tool calls whose end was missed while disconnected are over.
|
|
646
618
|
if (!isBusy(draft)) draft.tools = {}
|
|
647
619
|
for (const session of event.sessions) {
|
|
@@ -673,13 +645,9 @@ export function replayEffects({
|
|
|
673
645
|
const blocks = messages.flatMap((message): Block[] => {
|
|
674
646
|
if (message.type !== 'assistant') return []
|
|
675
647
|
return message.content.flatMap((part): Block[] => {
|
|
676
|
-
if (part.type === 'text') return part.text.trim()
|
|
648
|
+
if (part.type === 'text') return part.text.trim() ? [{ kind: 'text', text: part.text.trim() }] : []
|
|
677
649
|
if (part.type !== 'tool' || part.state.status === 'streaming') return []
|
|
678
650
|
const { input, metadata } = part.state
|
|
679
|
-
if (part.name === MESSAGE_TOOL) {
|
|
680
|
-
const text = stringInput(input, 'text').trim()
|
|
681
|
-
return text ? [{ kind: 'text', text }] : []
|
|
682
|
-
}
|
|
683
651
|
const isExecute = part.name === 'execute'
|
|
684
652
|
const lines = [{ name: part.name, input }, ...(isExecute ? executeCalls(metadata) : [])]
|
|
685
653
|
.filter((call) => isToolVisible(call, prefs.verbosity))
|
package/src/tunnel.test.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import http from 'node:http'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { createServer } from 'vite'
|
|
1
6
|
import { expect, test } from 'vitest'
|
|
2
|
-
import { detectPortFromText, routeName } from './tunnel.ts'
|
|
7
|
+
import { detectPortFromText, parseNetstatListeningPids, routeName, startHostProxy } from './tunnel.ts'
|
|
3
8
|
|
|
4
9
|
test('detects the dev server port and skips noise lines', () => {
|
|
5
10
|
const vite = '\u001b[33mPort 5173 is in use, trying another one...\u001b[39m\n ➜ Local: \u001b[36mhttp://localhost:\u001b[1m5174\u001b[22m/\u001b[39m\n'
|
|
@@ -25,15 +30,86 @@ test('detects the dev server port and skips noise lines', () => {
|
|
|
25
30
|
test('route name: explicit, then tuistory session, then random', () => {
|
|
26
31
|
expect({
|
|
27
32
|
explicit: routeName({ name: 'docs', tuistorySessionId: 'abc123' }),
|
|
28
|
-
|
|
29
|
-
|
|
33
|
+
normalized: routeName({ name: '_My_App.dev_' }),
|
|
34
|
+
invalid: String(routeName({ name: '___' })),
|
|
35
|
+
tuistory: routeName({ tuistorySessionId: 'Holocron_Dev' }),
|
|
36
|
+
tuistoryInvalid: String(routeName({ tuistorySessionId: '___' })).length,
|
|
30
37
|
random: String(routeName({})).length,
|
|
31
38
|
}).toMatchInlineSnapshot(`
|
|
32
39
|
{
|
|
33
40
|
"explicit": "docs",
|
|
34
|
-
"invalid": "Error: Invalid tunnel ID "
|
|
41
|
+
"invalid": "Error: Invalid tunnel ID "___": use letters, digits and dashes",
|
|
42
|
+
"normalized": "my-app-dev",
|
|
35
43
|
"random": 16,
|
|
36
|
-
"tuistory": "
|
|
44
|
+
"tuistory": "holocron-dev",
|
|
45
|
+
"tuistoryInvalid": 16,
|
|
37
46
|
}
|
|
38
47
|
`)
|
|
39
48
|
})
|
|
49
|
+
|
|
50
|
+
test('netstat listening pids on Windows', () => {
|
|
51
|
+
const stdout = [
|
|
52
|
+
'Active Connections',
|
|
53
|
+
' Proto Local Address Foreign Address State PID',
|
|
54
|
+
' TCP 0.0.0.0:5173 0.0.0.0:0 LISTENING 1234',
|
|
55
|
+
' TCP [::]:5173 [::]:0 LISTENING 1234',
|
|
56
|
+
' TCP 127.0.0.1:51730 0.0.0.0:0 LISTENING 999',
|
|
57
|
+
' TCP 127.0.0.1:5173 127.0.0.1:60000 ESTABLISHED 777',
|
|
58
|
+
' UDP 0.0.0.0:5173 *:* 555',
|
|
59
|
+
].join('\r\n')
|
|
60
|
+
expect(parseNetstatListeningPids({ stdout, port: 5173 })).toMatchInlineSnapshot(`
|
|
61
|
+
[
|
|
62
|
+
1234,
|
|
63
|
+
]
|
|
64
|
+
`)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
function request({ port, headers }: { port: number; headers: http.OutgoingHttpHeaders }): Promise<string> {
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
const req = http.request({ host: '127.0.0.1', port, path: '/', headers })
|
|
70
|
+
req.on('response', (res) => {
|
|
71
|
+
res.resume()
|
|
72
|
+
resolve(String(res.statusCode))
|
|
73
|
+
req.destroy()
|
|
74
|
+
})
|
|
75
|
+
req.on('upgrade', (res, socket) => {
|
|
76
|
+
resolve(`${res.statusCode} ${res.headers['sec-websocket-protocol']}`)
|
|
77
|
+
socket.destroy()
|
|
78
|
+
})
|
|
79
|
+
req.on('error', reject)
|
|
80
|
+
req.end()
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
test('host proxy lets the public tunnel host through Vite host check, HMR included', async () => {
|
|
85
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'kimaki-tunnel-'))
|
|
86
|
+
fs.writeFileSync(path.join(root, 'index.html'), '<p>hi</p>')
|
|
87
|
+
const vite = await createServer({ root, configFile: false, logLevel: 'silent', server: { host: '127.0.0.1', port: 0 } })
|
|
88
|
+
await vite.listen()
|
|
89
|
+
const address = vite.httpServer!.address()
|
|
90
|
+
if (!address || typeof address === 'string') throw new Error('no vite address')
|
|
91
|
+
const proxy = await startHostProxy({ host: '127.0.0.1', port: address.port })
|
|
92
|
+
if (proxy instanceof Error) throw proxy
|
|
93
|
+
const proxyAddress = proxy.address()
|
|
94
|
+
if (!proxyAddress || typeof proxyAddress === 'string') throw new Error('no proxy address')
|
|
95
|
+
|
|
96
|
+
const publicHost = { host: 'docs.abc.opentunnel.xyz' }
|
|
97
|
+
const hmr = { ...publicHost, connection: 'Upgrade', upgrade: 'websocket', 'sec-websocket-version': '13', 'sec-websocket-key': 'dGhlIHNhbXBsZSBub25jZQ==', 'sec-websocket-protocol': 'vite-hmr' }
|
|
98
|
+
try {
|
|
99
|
+
expect({
|
|
100
|
+
direct: await request({ port: address.port, headers: publicHost }),
|
|
101
|
+
proxied: await request({ port: proxyAddress.port, headers: publicHost }),
|
|
102
|
+
proxiedHmr: await request({ port: proxyAddress.port, headers: hmr }),
|
|
103
|
+
}).toMatchInlineSnapshot(`
|
|
104
|
+
{
|
|
105
|
+
"direct": "403",
|
|
106
|
+
"proxied": "200",
|
|
107
|
+
"proxiedHmr": "101 vite-hmr",
|
|
108
|
+
}
|
|
109
|
+
`)
|
|
110
|
+
} finally {
|
|
111
|
+
proxy.close()
|
|
112
|
+
await vite.close()
|
|
113
|
+
fs.rmSync(root, { recursive: true, force: true })
|
|
114
|
+
}
|
|
115
|
+
})
|