kimaki 0.6.0 → 0.7.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/anthropic-auth-plugin.js +10 -3
- package/dist/bundled-skills.js +37 -0
- package/dist/cli.js +3 -3
- package/dist/commands/add-dir.js +1 -1
- package/dist/commands/add-dir.test.js +32 -28
- package/dist/commands/btw.js +2 -2
- package/dist/commands/fork-subagent.js +177 -0
- package/dist/commands/fork.js +71 -29
- package/dist/discord-command-registration.js +7 -2
- package/dist/format-tables.js +197 -8
- package/dist/format-tables.test.js +153 -2
- package/dist/hrana-server.js +12 -24
- package/dist/interaction-handler.js +9 -1
- package/dist/kimaki-opencode-plugin-loading.e2e.test.js +12 -5
- package/dist/kimaki-opencode-plugin.js +0 -1
- package/dist/message-preprocessing.js +5 -4
- package/dist/message-preprocessing.test.js +35 -0
- package/dist/onboarding-tutorial.js +6 -15
- package/dist/opencode.js +13 -1
- package/dist/orphan-opencode-sweep.test.js +80 -0
- package/dist/session-handler/event-stream-state.js +29 -1
- package/dist/session-handler/event-stream-state.test.js +70 -1
- package/dist/store.js +1 -1
- package/dist/system-message.js +77 -30
- package/dist/system-message.test.js +88 -32
- package/dist/thread-message-queue.e2e.test.js +2 -2
- package/dist/tools.js +16 -24
- package/dist/voice.js +10 -1
- package/package.json +7 -6
- package/skills/batch/SKILL.md +1 -1
- package/skills/goke/SKILL.md +1 -1
- package/skills/new-skill/SKILL.md +1 -1
- package/skills/npm-package/SKILL.md +62 -23
- package/skills/profano/SKILL.md +5 -13
- package/skills/sigillo/SKILL.md +101 -0
- package/skills/spiceflow/SKILL.md +16 -2
- package/skills/tuistory/SKILL.md +60 -212
- package/skills/zele/SKILL.md +32 -124
- package/src/anthropic-auth-plugin.ts +13 -4
- package/src/cli.ts +3 -3
- package/src/commands/add-dir.test.ts +35 -28
- package/src/commands/add-dir.ts +1 -1
- package/src/commands/btw.ts +2 -2
- package/src/commands/fork-subagent.ts +263 -0
- package/src/commands/fork.ts +105 -40
- package/src/discord-command-registration.ts +7 -2
- package/src/format-tables.test.ts +168 -8
- package/src/format-tables.ts +282 -9
- package/src/hrana-server.ts +12 -27
- package/src/interaction-handler.ts +17 -1
- package/src/kimaki-opencode-plugin-loading.e2e.test.ts +13 -5
- package/src/kimaki-opencode-plugin.ts +0 -1
- package/src/message-preprocessing.ts +5 -4
- package/src/onboarding-tutorial.ts +6 -15
- package/src/opencode.ts +18 -1
- package/src/session-handler/event-stream-state.test.ts +74 -0
- package/src/session-handler/event-stream-state.ts +54 -2
- package/src/store.ts +1 -1
- package/src/system-message.test.ts +103 -44
- package/src/system-message.ts +77 -30
- package/src/thread-message-queue.e2e.test.ts +2 -2
- package/src/tools.ts +26 -41
- package/src/voice.ts +11 -0
- package/skills/jitter/dist/jitter-utils.js +0 -620
- package/src/bash-tool.test.ts +0 -103
- package/src/bash-tool.ts +0 -287
- package/src/system-prompt-drift-plugin.ts +0 -354
package/dist/format-tables.js
CHANGED
|
@@ -15,12 +15,75 @@ const MAX_COMPONENTS = 40;
|
|
|
15
15
|
* pairs. Large tables are split across multiple component segments.
|
|
16
16
|
*/
|
|
17
17
|
export function splitTablesFromMarkdown(markdown, options = {}) {
|
|
18
|
+
const blocks = splitMarkdownByCallouts({ markdown });
|
|
19
|
+
return blocks.flatMap((block) => {
|
|
20
|
+
if (block.type === 'callout') {
|
|
21
|
+
const innerSegments = splitTablesFromMarkdown(block.content, options);
|
|
22
|
+
return buildCalloutSegments({
|
|
23
|
+
segments: innerSegments,
|
|
24
|
+
callout: block.callout,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return splitTableSegmentsFromText({
|
|
28
|
+
markdown: block.text,
|
|
29
|
+
options,
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
function splitMarkdownByCallouts({ markdown, }) {
|
|
34
|
+
const lines = markdown.match(/.*(?:\n|$)/g)?.filter((line) => {
|
|
35
|
+
return line.length > 0;
|
|
36
|
+
}) ?? [markdown];
|
|
37
|
+
const blocks = [];
|
|
38
|
+
let textBuffer = '';
|
|
39
|
+
for (let index = 0; index < lines.length; index++) {
|
|
40
|
+
const line = lines[index];
|
|
41
|
+
const callout = parseCalloutOpenLine({ line });
|
|
42
|
+
if (!callout) {
|
|
43
|
+
textBuffer += line;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (textBuffer.length > 0) {
|
|
47
|
+
blocks.push({ type: 'text', text: textBuffer });
|
|
48
|
+
textBuffer = '';
|
|
49
|
+
}
|
|
50
|
+
const body = collectCalloutBodyFromLines({
|
|
51
|
+
lines,
|
|
52
|
+
startIndex: index,
|
|
53
|
+
});
|
|
54
|
+
if (body instanceof Error) {
|
|
55
|
+
textBuffer += line;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
blocks.push({
|
|
59
|
+
type: 'callout',
|
|
60
|
+
content: body.content,
|
|
61
|
+
callout,
|
|
62
|
+
});
|
|
63
|
+
index = body.endIndex;
|
|
64
|
+
}
|
|
65
|
+
if (textBuffer.length > 0) {
|
|
66
|
+
blocks.push({ type: 'text', text: textBuffer });
|
|
67
|
+
}
|
|
68
|
+
return blocks;
|
|
69
|
+
}
|
|
70
|
+
function splitTableSegmentsFromText({ markdown, options, }) {
|
|
18
71
|
const lexer = new Lexer();
|
|
19
|
-
|
|
72
|
+
return splitTokensIntoSegments({
|
|
73
|
+
tokens: lexer.lex(markdown),
|
|
74
|
+
options,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
function splitTokensIntoSegments({ tokens, options, }) {
|
|
20
78
|
const segments = [];
|
|
21
79
|
let textBuffer = '';
|
|
80
|
+
const isTableToken = (token) => {
|
|
81
|
+
return (token.type === 'table' &&
|
|
82
|
+
Object.hasOwn(token, 'header') &&
|
|
83
|
+
Object.hasOwn(token, 'rows'));
|
|
84
|
+
};
|
|
22
85
|
for (const token of tokens) {
|
|
23
|
-
if (token
|
|
86
|
+
if (isTableToken(token)) {
|
|
24
87
|
if (textBuffer.trim()) {
|
|
25
88
|
segments.push({ type: 'text', text: textBuffer });
|
|
26
89
|
textBuffer = '';
|
|
@@ -37,6 +100,126 @@ export function splitTablesFromMarkdown(markdown, options = {}) {
|
|
|
37
100
|
}
|
|
38
101
|
return segments;
|
|
39
102
|
}
|
|
103
|
+
function buildCalloutSegments({ segments, callout, }) {
|
|
104
|
+
const children = flattenCalloutChildren({ segments });
|
|
105
|
+
if (children.length === 0) {
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
const chunks = chunkCalloutChildrenByComponentLimit({ children });
|
|
109
|
+
return chunks.map((chunk) => {
|
|
110
|
+
const container = {
|
|
111
|
+
type: ComponentType.Container,
|
|
112
|
+
...(callout.accentColor !== undefined
|
|
113
|
+
? { accent_color: callout.accentColor }
|
|
114
|
+
: {}),
|
|
115
|
+
components: chunk,
|
|
116
|
+
};
|
|
117
|
+
const components = [container];
|
|
118
|
+
return {
|
|
119
|
+
type: 'components',
|
|
120
|
+
components,
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
function flattenCalloutChildren({ segments, }) {
|
|
125
|
+
return segments.flatMap((segment) => {
|
|
126
|
+
if (segment.type === 'text') {
|
|
127
|
+
if (!segment.text.trim()) {
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
return [
|
|
131
|
+
{
|
|
132
|
+
type: ComponentType.TextDisplay,
|
|
133
|
+
content: segment.text.trim(),
|
|
134
|
+
},
|
|
135
|
+
];
|
|
136
|
+
}
|
|
137
|
+
return segment.components.flatMap((component) => {
|
|
138
|
+
if (component.type !== ComponentType.Container) {
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
return component.components;
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
function chunkCalloutChildrenByComponentLimit({ children, }) {
|
|
146
|
+
const chunks = [];
|
|
147
|
+
let currentChunk = [];
|
|
148
|
+
for (const child of children) {
|
|
149
|
+
if (currentChunk.length > 0 && currentChunk.length + 2 > MAX_COMPONENTS) {
|
|
150
|
+
chunks.push(currentChunk);
|
|
151
|
+
currentChunk = [];
|
|
152
|
+
}
|
|
153
|
+
currentChunk.push(child);
|
|
154
|
+
}
|
|
155
|
+
if (currentChunk.length > 0) {
|
|
156
|
+
chunks.push(currentChunk);
|
|
157
|
+
}
|
|
158
|
+
return chunks;
|
|
159
|
+
}
|
|
160
|
+
function collectCalloutBodyFromLines({ lines, startIndex, }) {
|
|
161
|
+
let depth = 0;
|
|
162
|
+
const contentLines = [];
|
|
163
|
+
for (let index = startIndex; index < lines.length; index++) {
|
|
164
|
+
const line = lines[index];
|
|
165
|
+
const nestedCallout = parseCalloutOpenLine({ line });
|
|
166
|
+
if (nestedCallout) {
|
|
167
|
+
if (depth > 0) {
|
|
168
|
+
contentLines.push(line);
|
|
169
|
+
}
|
|
170
|
+
depth += 1;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (/^<\/callout>$/i.test(line.trim())) {
|
|
174
|
+
depth -= 1;
|
|
175
|
+
if (depth === 0) {
|
|
176
|
+
return {
|
|
177
|
+
content: contentLines.join(''),
|
|
178
|
+
endIndex: index,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
contentLines.push(line);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (depth > 0) {
|
|
185
|
+
contentLines.push(line);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return new Error('Unclosed <callout> block');
|
|
189
|
+
}
|
|
190
|
+
function parseCalloutOpenLine({ line, }) {
|
|
191
|
+
const match = line.trim().match(/^<callout(?:\s+[^>]*)?>$/i);
|
|
192
|
+
if (!match) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
const accentValue = line.match(/\baccent=(['"])(.*?)\1/i)?.[2]?.trim();
|
|
196
|
+
const accentColor = accentValue
|
|
197
|
+
? parseAccentColor({ value: accentValue })
|
|
198
|
+
: undefined;
|
|
199
|
+
return {
|
|
200
|
+
accentColor: accentColor instanceof Error ? undefined : accentColor,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function parseAccentColor({ value, }) {
|
|
204
|
+
const hex = value.trim().toLowerCase();
|
|
205
|
+
if (/^#[0-9a-f]{6}$/.test(hex)) {
|
|
206
|
+
return Number.parseInt(hex.slice(1), 16);
|
|
207
|
+
}
|
|
208
|
+
if (/^#[0-9a-f]{3}$/.test(hex)) {
|
|
209
|
+
const expanded = hex
|
|
210
|
+
.slice(1)
|
|
211
|
+
.split('')
|
|
212
|
+
.map((char) => {
|
|
213
|
+
return `${char}${char}`;
|
|
214
|
+
})
|
|
215
|
+
.join('');
|
|
216
|
+
return Number.parseInt(expanded, 16);
|
|
217
|
+
}
|
|
218
|
+
if (/^\d+$/.test(hex)) {
|
|
219
|
+
return Number.parseInt(hex, 10);
|
|
220
|
+
}
|
|
221
|
+
return new Error(`Unsupported callout accent color: ${value}`);
|
|
222
|
+
}
|
|
40
223
|
/**
|
|
41
224
|
* Build CV2 components for a table. Plain rows render as one TextDisplay with
|
|
42
225
|
* bold key-value lines. Rows with resolved button cells render as a TextDisplay
|
|
@@ -72,9 +255,10 @@ export function buildTableComponents(table, options = {}) {
|
|
|
72
255
|
type: ComponentType.Container,
|
|
73
256
|
components: children,
|
|
74
257
|
};
|
|
258
|
+
const components = [container];
|
|
75
259
|
return {
|
|
76
260
|
type: 'components',
|
|
77
|
-
components
|
|
261
|
+
components,
|
|
78
262
|
};
|
|
79
263
|
});
|
|
80
264
|
}
|
|
@@ -289,12 +473,17 @@ function extractTokenText(token) {
|
|
|
289
473
|
case 'br':
|
|
290
474
|
return ' ';
|
|
291
475
|
default: {
|
|
292
|
-
const
|
|
293
|
-
if (
|
|
294
|
-
return extractCellText(
|
|
476
|
+
const nestedTokens = Reflect.get(token, 'tokens');
|
|
477
|
+
if (Array.isArray(nestedTokens)) {
|
|
478
|
+
return extractCellText(nestedTokens.filter((value) => {
|
|
479
|
+
return (typeof value === 'object' &&
|
|
480
|
+
value !== null &&
|
|
481
|
+
typeof Reflect.get(value, 'type') === 'string');
|
|
482
|
+
}));
|
|
295
483
|
}
|
|
296
|
-
|
|
297
|
-
|
|
484
|
+
const text = Reflect.get(token, 'text');
|
|
485
|
+
if (typeof text === 'string') {
|
|
486
|
+
return text;
|
|
298
487
|
}
|
|
299
488
|
return '';
|
|
300
489
|
}
|
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
import { test, expect, describe } from 'vitest';
|
|
2
2
|
import { splitTablesFromMarkdown, buildTableComponents, } from './format-tables.js';
|
|
3
3
|
import { Lexer } from 'marked';
|
|
4
|
+
import { ComponentType } from 'discord.js';
|
|
5
|
+
function isTableToken(token) {
|
|
6
|
+
return (token.type === 'table' &&
|
|
7
|
+
Object.hasOwn(token, 'header') &&
|
|
8
|
+
Object.hasOwn(token, 'rows'));
|
|
9
|
+
}
|
|
4
10
|
function parseTable(markdown) {
|
|
5
11
|
const lexer = new Lexer();
|
|
6
12
|
const tokens = lexer.lex(markdown);
|
|
7
|
-
|
|
13
|
+
const table = tokens.find((token) => {
|
|
14
|
+
return isTableToken(token);
|
|
15
|
+
});
|
|
16
|
+
if (!table || !isTableToken(table)) {
|
|
17
|
+
throw new Error('Expected markdown to contain a table token');
|
|
18
|
+
}
|
|
19
|
+
return table;
|
|
8
20
|
}
|
|
9
21
|
/** Extract the first container's children from buildTableComponents result */
|
|
10
22
|
function getContainerChildren(segments) {
|
|
@@ -13,7 +25,20 @@ function getContainerChildren(segments) {
|
|
|
13
25
|
throw new Error('Expected components segment');
|
|
14
26
|
}
|
|
15
27
|
const container = seg.components[0];
|
|
16
|
-
|
|
28
|
+
if (!container || container.type !== ComponentType.Container) {
|
|
29
|
+
throw new Error('Expected first top-level component to be a container');
|
|
30
|
+
}
|
|
31
|
+
return container.components.map((component) => {
|
|
32
|
+
const content = component.type === ComponentType.TextDisplay ? component.content : undefined;
|
|
33
|
+
const divider = component.type === ComponentType.Separator ? component.divider : undefined;
|
|
34
|
+
const spacing = component.type === ComponentType.Separator ? component.spacing : undefined;
|
|
35
|
+
return {
|
|
36
|
+
type: component.type,
|
|
37
|
+
content,
|
|
38
|
+
divider,
|
|
39
|
+
spacing,
|
|
40
|
+
};
|
|
41
|
+
});
|
|
17
42
|
}
|
|
18
43
|
describe('buildTableComponents', () => {
|
|
19
44
|
test('builds container with key-value TextDisplays', () => {
|
|
@@ -305,4 +330,130 @@ Done.`);
|
|
|
305
330
|
]
|
|
306
331
|
`);
|
|
307
332
|
});
|
|
333
|
+
test('renders callout text inside an accented container', () => {
|
|
334
|
+
const result = splitTablesFromMarkdown(`<callout accent="#2b7fff">
|
|
335
|
+
## Important
|
|
336
|
+
|
|
337
|
+
Read this first.
|
|
338
|
+
</callout>`);
|
|
339
|
+
expect(result).toMatchInlineSnapshot(`
|
|
340
|
+
[
|
|
341
|
+
{
|
|
342
|
+
"components": [
|
|
343
|
+
{
|
|
344
|
+
"accent_color": 2850815,
|
|
345
|
+
"components": [
|
|
346
|
+
{
|
|
347
|
+
"content": "## Important
|
|
348
|
+
|
|
349
|
+
Read this first.",
|
|
350
|
+
"type": 10,
|
|
351
|
+
},
|
|
352
|
+
],
|
|
353
|
+
"type": 17,
|
|
354
|
+
},
|
|
355
|
+
],
|
|
356
|
+
"type": "components",
|
|
357
|
+
},
|
|
358
|
+
]
|
|
359
|
+
`);
|
|
360
|
+
});
|
|
361
|
+
test('renders tables inside callouts recursively', () => {
|
|
362
|
+
const result = splitTablesFromMarkdown(`<callout accent="#2b7fff">
|
|
363
|
+
## Important
|
|
364
|
+
|
|
365
|
+
| Key | Value |
|
|
366
|
+
| --- | --- |
|
|
367
|
+
| a | 1 |
|
|
368
|
+
</callout>`);
|
|
369
|
+
expect(result).toMatchInlineSnapshot(`
|
|
370
|
+
[
|
|
371
|
+
{
|
|
372
|
+
"components": [
|
|
373
|
+
{
|
|
374
|
+
"accent_color": 2850815,
|
|
375
|
+
"components": [
|
|
376
|
+
{
|
|
377
|
+
"content": "## Important",
|
|
378
|
+
"type": 10,
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
"content": "**Key** a
|
|
382
|
+
**Value** 1",
|
|
383
|
+
"type": 10,
|
|
384
|
+
},
|
|
385
|
+
],
|
|
386
|
+
"type": 17,
|
|
387
|
+
},
|
|
388
|
+
],
|
|
389
|
+
"type": "components",
|
|
390
|
+
},
|
|
391
|
+
]
|
|
392
|
+
`);
|
|
393
|
+
});
|
|
394
|
+
test('renders button rows inside callouts recursively', () => {
|
|
395
|
+
const result = splitTablesFromMarkdown(`<callout accent="#2b7fff">
|
|
396
|
+
## Actions
|
|
397
|
+
|
|
398
|
+
| Name | Action |
|
|
399
|
+
| --- | --- |
|
|
400
|
+
| feature-a | <button id="delete-a" variant="secondary">Delete</button> |
|
|
401
|
+
</callout>`, {
|
|
402
|
+
resolveButtonCustomId: ({ button }) => {
|
|
403
|
+
return `html_action:${button.id}`;
|
|
404
|
+
},
|
|
405
|
+
});
|
|
406
|
+
expect(result).toMatchInlineSnapshot(`
|
|
407
|
+
[
|
|
408
|
+
{
|
|
409
|
+
"components": [
|
|
410
|
+
{
|
|
411
|
+
"accent_color": 2850815,
|
|
412
|
+
"components": [
|
|
413
|
+
{
|
|
414
|
+
"content": "## Actions",
|
|
415
|
+
"type": 10,
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
"content": "**Name** feature-a",
|
|
419
|
+
"type": 10,
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
"components": [
|
|
423
|
+
{
|
|
424
|
+
"custom_id": "html_action:delete-a",
|
|
425
|
+
"disabled": false,
|
|
426
|
+
"label": "Delete",
|
|
427
|
+
"style": 2,
|
|
428
|
+
"type": 2,
|
|
429
|
+
},
|
|
430
|
+
],
|
|
431
|
+
"type": 1,
|
|
432
|
+
},
|
|
433
|
+
],
|
|
434
|
+
"type": 17,
|
|
435
|
+
},
|
|
436
|
+
],
|
|
437
|
+
"type": "components",
|
|
438
|
+
},
|
|
439
|
+
]
|
|
440
|
+
`);
|
|
441
|
+
});
|
|
442
|
+
test('falls back to plain text when a callout is not closed', () => {
|
|
443
|
+
const result = splitTablesFromMarkdown(`<callout accent="#2b7fff">
|
|
444
|
+
## Important
|
|
445
|
+
|
|
446
|
+
Still open`);
|
|
447
|
+
expect(result).toMatchInlineSnapshot(`
|
|
448
|
+
[
|
|
449
|
+
{
|
|
450
|
+
"text": "<callout accent="#2b7fff">
|
|
451
|
+
## Important
|
|
452
|
+
|
|
453
|
+
Still open",
|
|
454
|
+
"type": "text",
|
|
455
|
+
},
|
|
456
|
+
]
|
|
457
|
+
`);
|
|
458
|
+
});
|
|
308
459
|
});
|
package/dist/hrana-server.js
CHANGED
|
@@ -235,29 +235,17 @@ export async function evictExistingInstance({ port }) {
|
|
|
235
235
|
hranaLogger.log(`Failed to kill PID ${targetPid}: ${killResult.message}`);
|
|
236
236
|
return;
|
|
237
237
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
try: () => {
|
|
250
|
-
process.kill(targetPid, 'SIGKILL');
|
|
251
|
-
},
|
|
252
|
-
catch: (e) => new Error('Failed to send SIGKILL to existing kimaki process', {
|
|
253
|
-
cause: e,
|
|
254
|
-
}),
|
|
255
|
-
});
|
|
256
|
-
if (forceKillResult instanceof Error) {
|
|
257
|
-
hranaLogger.log(`Failed to force-kill PID ${targetPid}: ${forceKillResult.message}`);
|
|
258
|
-
return;
|
|
238
|
+
for (let attempt = 0; attempt < 10; attempt += 1) {
|
|
239
|
+
await new Promise((resolve) => {
|
|
240
|
+
setTimeout(resolve, 1000);
|
|
241
|
+
});
|
|
242
|
+
// Verify it's gone. Some shutdown paths need a few seconds to run cleanup,
|
|
243
|
+
// so we avoid SIGKILL and just poll for up to 10 seconds.
|
|
244
|
+
const secondProbe = await fetch(url, {
|
|
245
|
+
signal: AbortSignal.timeout(2000),
|
|
246
|
+
}).catch((e) => new FetchError({ url, cause: e }));
|
|
247
|
+
if (secondProbe instanceof Error)
|
|
248
|
+
return;
|
|
259
249
|
}
|
|
260
|
-
|
|
261
|
-
setTimeout(resolve, 1000);
|
|
262
|
-
});
|
|
250
|
+
hranaLogger.log(`PID ${targetPid} still alive after 10s SIGTERM grace period`);
|
|
263
251
|
}
|
|
@@ -18,7 +18,8 @@ import { handleAddDirCommand } from './commands/add-dir.js';
|
|
|
18
18
|
import { handleCompactCommand } from './commands/compact.js';
|
|
19
19
|
import { handleShareCommand } from './commands/share.js';
|
|
20
20
|
import { handleDiffCommand } from './commands/diff.js';
|
|
21
|
-
import { handleForkCommand, handleForkSelectMenu } from './commands/fork.js';
|
|
21
|
+
import { handleForkCommand, handleForkSelectMenu, } from './commands/fork.js';
|
|
22
|
+
import { handleForkSubagentCommand, handleForkSubagentSelectMenu, } from './commands/fork-subagent.js';
|
|
22
23
|
import { handleBtwCommand } from './commands/btw.js';
|
|
23
24
|
import { handleModelCommand, handleProviderSelectMenu, handleModelSelectMenu, handleModelScopeSelectMenu, } from './commands/model.js';
|
|
24
25
|
import { handleUnsetModelCommand } from './commands/unset-model.js';
|
|
@@ -154,6 +155,9 @@ export function registerInteractionHandler({ discordClient, appId, }) {
|
|
|
154
155
|
case 'fork':
|
|
155
156
|
await handleForkCommand(interaction);
|
|
156
157
|
return;
|
|
158
|
+
case 'fork-subagent':
|
|
159
|
+
await handleForkSubagentCommand(interaction);
|
|
160
|
+
return;
|
|
157
161
|
case 'btw':
|
|
158
162
|
await handleBtwCommand({ command: interaction, appId });
|
|
159
163
|
return;
|
|
@@ -306,6 +310,10 @@ export function registerInteractionHandler({ discordClient, appId, }) {
|
|
|
306
310
|
await handleForkSelectMenu(interaction);
|
|
307
311
|
return;
|
|
308
312
|
}
|
|
313
|
+
if (customId.startsWith('fork_subagent_select:')) {
|
|
314
|
+
await handleForkSubagentSelectMenu(interaction);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
309
317
|
if (customId.startsWith('model_provider:')) {
|
|
310
318
|
await handleProviderSelectMenu(interaction);
|
|
311
319
|
return;
|
|
@@ -35,6 +35,17 @@ test('opencode server loads plugin without errors', async () => {
|
|
|
35
35
|
const pluginPath = new URL('../src/kimaki-opencode-plugin.ts', import.meta.url).href;
|
|
36
36
|
const stderrLines = [];
|
|
37
37
|
const isolatedOpencodeRoot = path.join(projectDir, 'opencode-test-home');
|
|
38
|
+
const xdgDirectories = {
|
|
39
|
+
OPENCODE_CONFIG_DIR: path.join(isolatedOpencodeRoot, '.opencode-kimaki'),
|
|
40
|
+
XDG_CONFIG_HOME: path.join(isolatedOpencodeRoot, '.config'),
|
|
41
|
+
XDG_DATA_HOME: path.join(isolatedOpencodeRoot, '.local', 'share'),
|
|
42
|
+
XDG_CACHE_HOME: path.join(isolatedOpencodeRoot, '.cache'),
|
|
43
|
+
XDG_STATE_HOME: path.join(isolatedOpencodeRoot, '.local', 'state'),
|
|
44
|
+
};
|
|
45
|
+
fs.mkdirSync(isolatedOpencodeRoot, { recursive: true });
|
|
46
|
+
Object.values(xdgDirectories).forEach((directory) => {
|
|
47
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
48
|
+
});
|
|
38
49
|
const { command, args, windowsVerbatimArguments, } = getSpawnCommandAndArgs({
|
|
39
50
|
resolvedCommand: resolveOpencodeCommand(),
|
|
40
51
|
baseArgs: ['serve', '--port', port.toString(), '--print-logs', '--log-level', 'DEBUG'],
|
|
@@ -52,11 +63,7 @@ test('opencode server loads plugin without errors', async () => {
|
|
|
52
63
|
plugin: [pluginPath],
|
|
53
64
|
}),
|
|
54
65
|
OPENCODE_TEST_HOME: isolatedOpencodeRoot,
|
|
55
|
-
|
|
56
|
-
XDG_CONFIG_HOME: path.join(isolatedOpencodeRoot, '.config'),
|
|
57
|
-
XDG_DATA_HOME: path.join(isolatedOpencodeRoot, '.local', 'share'),
|
|
58
|
-
XDG_CACHE_HOME: path.join(isolatedOpencodeRoot, '.cache'),
|
|
59
|
-
XDG_STATE_HOME: path.join(isolatedOpencodeRoot, '.local', 'state'),
|
|
66
|
+
...xdgDirectories,
|
|
60
67
|
},
|
|
61
68
|
});
|
|
62
69
|
serverProcess.stderr?.on('data', (data) => {
|
|
@@ -14,7 +14,6 @@ export { ipcToolsPlugin } from './ipc-tools-plugin.js';
|
|
|
14
14
|
export { contextAwarenessPlugin } from './context-awareness-plugin.js';
|
|
15
15
|
export { memoryOverviewPlugin } from './memory-overview-plugin.js';
|
|
16
16
|
export { interruptOpencodeSessionOnUserMessage } from './opencode-interrupt-plugin.js';
|
|
17
|
-
export { systemPromptDriftPlugin } from './system-prompt-drift-plugin.js';
|
|
18
17
|
export { anthropicAuthPlugin } from './anthropic-auth-plugin.js';
|
|
19
18
|
export { imageOptimizerPlugin } from './image-optimizer-plugin.js';
|
|
20
19
|
export { subagentRateLimitPlugin } from './subagent-rate-limit-plugin.js';
|
|
@@ -37,12 +37,13 @@ async function fetchAvailableAgents(getClient, directory) {
|
|
|
37
37
|
return { name: a.name, description: a.description };
|
|
38
38
|
});
|
|
39
39
|
}
|
|
40
|
-
// Matches
|
|
41
|
-
//
|
|
42
|
-
//
|
|
40
|
+
// Matches explicit queue markers at the end of a message (case-insensitive).
|
|
41
|
+
// Supported forms:
|
|
42
|
+
// - punctuation + queue: ". queue", "! queue", ". queue.", "!queue."
|
|
43
|
+
// - queue as its own final line: "text\nqueue" or just "queue"
|
|
43
44
|
// When present the suffix is stripped and the message is routed through
|
|
44
45
|
// kimaki's local queue (same as /queue command).
|
|
45
|
-
const QUEUE_SUFFIX_RE = /[.!?,;:]\s*queue\.?\s*$/i;
|
|
46
|
+
const QUEUE_SUFFIX_RE = /(?:[.!?,;:]|^)\s*queue\.?\s*$|\n\s*queue\.?\s*$/i;
|
|
46
47
|
const REPLIED_MESSAGE_TEXT_LIMIT = 1_000;
|
|
47
48
|
function extractQueueSuffix(prompt) {
|
|
48
49
|
if (!QUEUE_SUFFIX_RE.test(prompt)) {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Tests queue suffix parsing for incoming Discord messages.
|
|
2
|
+
import { describe, expect, test } from 'vitest';
|
|
3
|
+
import { extractQueueSuffix } from './message-preprocessing.js';
|
|
4
|
+
describe('extractQueueSuffix', () => {
|
|
5
|
+
test('supports queue as its own final line', () => {
|
|
6
|
+
expect([
|
|
7
|
+
extractQueueSuffix('Fix the bug\nqueue'),
|
|
8
|
+
extractQueueSuffix('Fix the bug\n queue.'),
|
|
9
|
+
extractQueueSuffix('queue'),
|
|
10
|
+
]).toMatchInlineSnapshot(`
|
|
11
|
+
[
|
|
12
|
+
{
|
|
13
|
+
"forceQueue": true,
|
|
14
|
+
"prompt": "Fix the bug",
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"forceQueue": true,
|
|
18
|
+
"prompt": "Fix the bug",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"forceQueue": true,
|
|
22
|
+
"prompt": "",
|
|
23
|
+
},
|
|
24
|
+
]
|
|
25
|
+
`);
|
|
26
|
+
});
|
|
27
|
+
test('does not treat plain trailing queue word on same line as a queue marker', () => {
|
|
28
|
+
expect(extractQueueSuffix('Tell me about queue')).toMatchInlineSnapshot(`
|
|
29
|
+
{
|
|
30
|
+
"forceQueue": false,
|
|
31
|
+
"prompt": "Tell me about queue",
|
|
32
|
+
}
|
|
33
|
+
`);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -29,21 +29,13 @@ ${backticks}bash
|
|
|
29
29
|
curl -fsSL https://bun.sh/install | bash
|
|
30
30
|
${backticks}
|
|
31
31
|
|
|
32
|
-
**
|
|
32
|
+
**tuistory** — needed to run the dev server in the background with kimaki tunnel:
|
|
33
33
|
|
|
34
34
|
${backticks}bash
|
|
35
|
-
|
|
35
|
+
bunx tuistory --help
|
|
36
36
|
${backticks}
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
${backticks}bash
|
|
41
|
-
# macOS
|
|
42
|
-
brew install tmux
|
|
43
|
-
|
|
44
|
-
# Ubuntu/Debian
|
|
45
|
-
sudo apt-get install tmux
|
|
46
|
-
${backticks}
|
|
38
|
+
This works without installing it globally because \`bunx\` can run it on demand.
|
|
47
39
|
|
|
48
40
|
Do NOT use Node.js, npm, or npx. Use Bun for everything.
|
|
49
41
|
|
|
@@ -140,15 +132,14 @@ Pick a random port between 3000-9000 to avoid conflicts:
|
|
|
140
132
|
|
|
141
133
|
${backticks}bash
|
|
142
134
|
PORT=$((RANDOM % 6000 + 3000))
|
|
143
|
-
|
|
144
|
-
tmux new-session -d -s game-dev -c "$PWD"
|
|
145
|
-
tmux send-keys -t game-dev "PORT=$PORT kimaki tunnel -p $PORT -- bun run server.ts" Enter
|
|
135
|
+
bunx tuistory launch "PORT=$PORT kimaki tunnel -p $PORT -- bun run server.ts" -s game-dev --cwd "$PWD"
|
|
146
136
|
${backticks}
|
|
147
137
|
|
|
148
138
|
Wait a moment, then get the tunnel URL:
|
|
149
139
|
|
|
150
140
|
${backticks}bash
|
|
151
|
-
|
|
141
|
+
bunx tuistory -s game-dev wait "/tunnel|https?:\/\//i" --timeout 30000
|
|
142
|
+
bunx tuistory read -s game-dev
|
|
152
143
|
${backticks}
|
|
153
144
|
|
|
154
145
|
If the tunnel URL is not visible yet, run the capture command again — it usually appears within a few seconds.
|
package/dist/opencode.js
CHANGED
|
@@ -337,6 +337,11 @@ async function waitForServer({ port, directory, maxAttempts = 300, startupStderr
|
|
|
337
337
|
// In-flight promise to prevent concurrent startups from racing
|
|
338
338
|
let startingServer = null;
|
|
339
339
|
let preferredStartupDirectory = null;
|
|
340
|
+
function ensureOpencodeHomeDirectories({ directories, }) {
|
|
341
|
+
Object.values(directories).map((directory) => {
|
|
342
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
343
|
+
});
|
|
344
|
+
}
|
|
340
345
|
async function ensureSingleServer({ directory, } = {}) {
|
|
341
346
|
const startupDirectory = directory || preferredStartupDirectory || undefined;
|
|
342
347
|
if (singleServer && !singleServer.process.killed) {
|
|
@@ -422,7 +427,7 @@ async function startSingleServer({ directory, } = {}) {
|
|
|
422
427
|
return {};
|
|
423
428
|
}
|
|
424
429
|
const root = path.join(getDataDir(), 'opencode-vitest-home');
|
|
425
|
-
|
|
430
|
+
const directories = {
|
|
426
431
|
OPENCODE_TEST_HOME: root,
|
|
427
432
|
OPENCODE_CONFIG_DIR: path.join(root, '.opencode-kimaki'),
|
|
428
433
|
XDG_CONFIG_HOME: path.join(root, '.config'),
|
|
@@ -430,6 +435,13 @@ async function startSingleServer({ directory, } = {}) {
|
|
|
430
435
|
XDG_CACHE_HOME: path.join(root, '.cache'),
|
|
431
436
|
XDG_STATE_HOME: path.join(root, '.local', 'state'),
|
|
432
437
|
};
|
|
438
|
+
// OpenCode writes state/config files into these XDG locations during boot.
|
|
439
|
+
// In CI, a fresh temp data dir means the parent folders may not exist yet,
|
|
440
|
+
// and some writes fail closed with NotFound before OpenCode has a chance to
|
|
441
|
+
// create them lazily. Pre-create the directories so startup-time tests do
|
|
442
|
+
// not flap based on process scheduling.
|
|
443
|
+
ensureOpencodeHomeDirectories({ directories });
|
|
444
|
+
return directories;
|
|
433
445
|
})();
|
|
434
446
|
// Write config to a file instead of passing via OPENCODE_CONFIG_CONTENT env var.
|
|
435
447
|
// OPENCODE_CONFIG (file path) is loaded before project config in opencode's
|