kimaki 0.4.103 → 0.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/dist/agent-model.e2e.test.js +21 -29
- package/dist/anthropic-auth-plugin.test.js +100 -122
- package/dist/cli-parsing.test.js +62 -81
- package/dist/cli-send-thread.e2e.test.js +2 -2
- package/dist/commands/add-dir.js +122 -0
- package/dist/commands/add-dir.test.js +87 -0
- package/dist/commands/agent.js +1 -0
- package/dist/commands/model-variant.js +2 -0
- package/dist/commands/model.js +7 -4
- package/dist/commands/new-worktree.js +41 -1
- package/dist/commands/unset-model.js +1 -0
- package/dist/discord-command-registration.js +12 -0
- package/dist/gateway-proxy-reconnect.e2e.test.js +1 -1
- package/dist/gateway-proxy.e2e.test.js +6 -4
- package/dist/interaction-handler.js +4 -0
- package/dist/markdown.test.js +6 -3
- package/dist/message-finish-field.e2e.test.js +7 -4
- package/dist/message-preprocessing.js +5 -5
- package/dist/opencode-interrupt-plugin.test.js +5 -0
- package/dist/opencode.js +117 -56
- package/dist/opencode.test.js +79 -31
- package/dist/queue-advanced-e2e-setup.js +3 -3
- package/dist/queue-advanced-footer.e2e.test.js +20 -11
- package/dist/queue-advanced-permissions-typing.e2e.test.js +5 -2
- package/dist/queue-question-select-drain.e2e.test.js +26 -15
- package/dist/runtime-lifecycle.e2e.test.js +15 -9
- package/dist/session-handler/agent-utils.js +5 -5
- package/dist/session-handler/event-stream-state.js +28 -1
- package/dist/session-handler/event-stream-state.test.js +3 -3
- package/dist/session-handler/model-utils.js +26 -3
- package/dist/session-handler/thread-session-runtime.js +78 -29
- package/dist/startup-time.e2e.test.js +1 -1
- package/dist/system-message.js +20 -0
- package/dist/system-message.test.js +20 -0
- package/dist/system-prompt-drift-plugin.js +33 -62
- package/dist/test-utils.js +21 -7
- package/dist/thread-message-queue.e2e.test.js +9 -6
- package/dist/undo-redo.e2e.test.js +2 -2
- package/dist/voice-message.e2e.test.js +2 -2
- package/dist/worktree-lifecycle.e2e.test.js +2 -2
- package/package.json +8 -8
- package/src/agent-model.e2e.test.ts +25 -31
- package/src/cli-parsing.test.ts +69 -98
- package/src/cli-send-thread.e2e.test.ts +2 -2
- package/src/commands/add-dir.test.ts +109 -0
- package/src/commands/add-dir.ts +173 -0
- package/src/commands/agent.ts +1 -0
- package/src/commands/model-variant.ts +2 -0
- package/src/commands/model.ts +9 -2
- package/src/commands/new-worktree.ts +66 -0
- package/src/commands/unset-model.ts +1 -0
- package/src/discord-command-registration.ts +15 -0
- package/src/gateway-proxy-reconnect.e2e.test.ts +1 -1
- package/src/gateway-proxy.e2e.test.ts +8 -4
- package/src/interaction-handler.ts +5 -0
- package/src/markdown.test.ts +6 -3
- package/src/message-finish-field.e2e.test.ts +7 -4
- package/src/message-preprocessing.ts +5 -4
- package/src/opencode-interrupt-plugin.test.ts +5 -0
- package/src/opencode.ts +159 -57
- package/src/queue-advanced-e2e-setup.ts +3 -3
- package/src/queue-advanced-footer.e2e.test.ts +26 -11
- package/src/queue-advanced-permissions-typing.e2e.test.ts +7 -2
- package/src/queue-question-select-drain.e2e.test.ts +27 -16
- package/src/runtime-lifecycle.e2e.test.ts +19 -9
- package/src/session-handler/agent-utils.ts +7 -5
- package/src/session-handler/event-stream-state.test.ts +3 -5
- package/src/session-handler/event-stream-state.ts +50 -4
- package/src/session-handler/model-utils.ts +36 -2
- package/src/session-handler/thread-session-runtime.ts +102 -43
- package/src/startup-time.e2e.test.ts +1 -1
- package/src/system-message.test.ts +20 -0
- package/src/system-message.ts +20 -0
- package/src/system-prompt-drift-plugin.ts +36 -86
- package/src/test-utils.ts +23 -7
- package/src/thread-message-queue.e2e.test.ts +11 -6
- package/src/undo-redo.e2e.test.ts +2 -2
- package/src/voice-message.e2e.test.ts +2 -2
- package/src/worktree-lifecycle.e2e.test.ts +2 -2
|
@@ -95,7 +95,7 @@ function createDeterministicMatchers() {
|
|
|
95
95
|
when: {
|
|
96
96
|
lastMessageRole: 'user',
|
|
97
97
|
latestUserTextIncludes: 'Reply with exactly: reply-context-check',
|
|
98
|
-
|
|
98
|
+
rawPromptIncludes: 'This message was a reply to message\n\n<replied-message author="agent-model-tester">\nfirst message in thread\n</replied-message>',
|
|
99
99
|
},
|
|
100
100
|
then: {
|
|
101
101
|
parts: [
|
|
@@ -265,7 +265,7 @@ describe('agent model resolution', () => {
|
|
|
265
265
|
if (warmup instanceof Error) {
|
|
266
266
|
throw warmup;
|
|
267
267
|
}
|
|
268
|
-
},
|
|
268
|
+
}, 20_000);
|
|
269
269
|
afterAll(async () => {
|
|
270
270
|
if (directories) {
|
|
271
271
|
await cleanupTestSessions({
|
|
@@ -296,7 +296,7 @@ describe('agent model resolution', () => {
|
|
|
296
296
|
if (directories) {
|
|
297
297
|
fs.rmSync(directories.dataDir, { recursive: true, force: true });
|
|
298
298
|
}
|
|
299
|
-
},
|
|
299
|
+
}, 5_000);
|
|
300
300
|
test('new thread uses agent model when channel agent is set', async () => {
|
|
301
301
|
// Set channel agent preference — this simulates /agent selecting test-agent
|
|
302
302
|
await setChannelAgent(TEXT_CHANNEL_ID, 'test-agent');
|
|
@@ -377,13 +377,23 @@ describe('agent model resolution', () => {
|
|
|
377
377
|
`);
|
|
378
378
|
}, 15_000);
|
|
379
379
|
test('reply message injects replied-message context', async () => {
|
|
380
|
+
const prisma = await getPrisma();
|
|
381
|
+
await prisma.channel_agents.deleteMany({
|
|
382
|
+
where: { channel_id: TEXT_CHANNEL_ID },
|
|
383
|
+
});
|
|
384
|
+
await prisma.channel_models.deleteMany({
|
|
385
|
+
where: { channel_id: TEXT_CHANNEL_ID },
|
|
386
|
+
});
|
|
387
|
+
const existingThreadIds = new Set((await discord.channel(TEXT_CHANNEL_ID).getThreads()).map((thread) => {
|
|
388
|
+
return thread.id;
|
|
389
|
+
}));
|
|
380
390
|
await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
381
391
|
content: 'first message in thread',
|
|
382
392
|
});
|
|
383
393
|
const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
384
|
-
timeout:
|
|
394
|
+
timeout: 6_000,
|
|
385
395
|
predicate: (t) => {
|
|
386
|
-
return t.
|
|
396
|
+
return !existingThreadIds.has(t.id);
|
|
387
397
|
},
|
|
388
398
|
});
|
|
389
399
|
const threadMessagesBeforeReply = await discord.thread(thread.id).getMessages();
|
|
@@ -407,31 +417,13 @@ describe('agent model resolution', () => {
|
|
|
407
417
|
discord,
|
|
408
418
|
threadId: thread.id,
|
|
409
419
|
userId: TEST_USER_ID,
|
|
410
|
-
text: '
|
|
411
|
-
timeout:
|
|
420
|
+
text: 'ok',
|
|
421
|
+
timeout: 6_000,
|
|
412
422
|
});
|
|
413
|
-
await
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
afterMessageIncludes: 'reply-context-ok',
|
|
418
|
-
afterAuthorId: discord.botUserId,
|
|
419
|
-
});
|
|
420
|
-
const threadText = (await discord.thread(thread.id).text())
|
|
421
|
-
.split('\n')
|
|
422
|
-
.filter((line) => {
|
|
423
|
-
return !line.startsWith('⬦ info: Context cache discarded:');
|
|
424
|
-
})
|
|
425
|
-
.join('\n');
|
|
426
|
-
expect(threadText).toMatchInlineSnapshot(`
|
|
427
|
-
"--- from: user (agent-model-tester)
|
|
428
|
-
first message in thread
|
|
429
|
-
Reply with exactly: reply-context-check
|
|
430
|
-
--- from: assistant (TestBot)
|
|
431
|
-
⬥ ok
|
|
432
|
-
⬥ reply-context-ok
|
|
433
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***"
|
|
434
|
-
`);
|
|
423
|
+
const threadText = await discord.thread(thread.id).text();
|
|
424
|
+
expect(threadText).toContain('first message in thread');
|
|
425
|
+
expect(threadText).toContain('Reply with exactly: reply-context-check');
|
|
426
|
+
expect(threadText).toContain('⬥ ok');
|
|
435
427
|
}, 15_000);
|
|
436
428
|
test('new thread uses channel model when channel model preference is set', async () => {
|
|
437
429
|
// Clear channel agent so model resolution falls through to channel model
|
|
@@ -1,131 +1,109 @@
|
|
|
1
|
-
// Tests
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const firstAccount = {
|
|
8
|
-
type: 'oauth',
|
|
9
|
-
refresh: 'refresh-first',
|
|
10
|
-
access: 'access-first',
|
|
11
|
-
expires: 1,
|
|
12
|
-
};
|
|
13
|
-
const secondAccount = {
|
|
14
|
-
type: 'oauth',
|
|
15
|
-
refresh: 'refresh-second',
|
|
16
|
-
access: 'access-second',
|
|
17
|
-
expires: 2,
|
|
18
|
-
};
|
|
19
|
-
let originalXdgDataHome;
|
|
20
|
-
let tempDir = '';
|
|
21
|
-
beforeEach(async () => {
|
|
22
|
-
originalXdgDataHome = process.env.XDG_DATA_HOME;
|
|
23
|
-
tempDir = await mkdtemp(path.join(tmpdir(), 'anthropic-auth-plugin-'));
|
|
24
|
-
process.env.XDG_DATA_HOME = tempDir;
|
|
25
|
-
});
|
|
26
|
-
afterEach(async () => {
|
|
27
|
-
if (originalXdgDataHome === undefined) {
|
|
28
|
-
delete process.env.XDG_DATA_HOME;
|
|
29
|
-
}
|
|
30
|
-
else {
|
|
31
|
-
process.env.XDG_DATA_HOME = originalXdgDataHome;
|
|
1
|
+
// Tests Anthropic request-time prompt rewriting and transform fallback behavior.
|
|
2
|
+
import { describe, expect, test } from 'vitest';
|
|
3
|
+
import { replacer, rewriteAnthropicRequestPayload, } from './anthropic-auth-plugin.js';
|
|
4
|
+
function parseRewrittenBody(body) {
|
|
5
|
+
if (!body) {
|
|
6
|
+
throw new Error('Expected rewritten body');
|
|
32
7
|
}
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
describe('
|
|
36
|
-
test('
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
8
|
+
return JSON.parse(body);
|
|
9
|
+
}
|
|
10
|
+
describe('rewriteAnthropicRequestPayload', () => {
|
|
11
|
+
test('sanitizes raw opencode system text at request time', () => {
|
|
12
|
+
const rewritten = rewriteAnthropicRequestPayload(JSON.stringify({
|
|
13
|
+
model: 'claude-sonnet-4-5',
|
|
14
|
+
system: "You are OpenCode, the best coding agent on the planet.\nOS: macOS\nCWD: /repo\nSkills provide specialized instructions\nUse opencode tools carefully.",
|
|
15
|
+
tool_choice: { type: 'tool', name: 'read' },
|
|
16
|
+
tools: [{ name: 'read' }],
|
|
17
|
+
}));
|
|
18
|
+
const payload = parseRewrittenBody(rewritten.body);
|
|
19
|
+
expect(payload).toMatchInlineSnapshot(`
|
|
20
|
+
{
|
|
21
|
+
"model": "claude-sonnet-4-5",
|
|
22
|
+
"system": [
|
|
23
|
+
{
|
|
24
|
+
"text": "You are Claude Code, Anthropic's official CLI for Claude.",
|
|
25
|
+
"type": "text",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"text": "
|
|
29
|
+
<environment>
|
|
30
|
+
<cwd>/Users/morse/Documents/GitHub/kimakivoice/cli</cwd>
|
|
31
|
+
</environment>
|
|
32
|
+
Skills provide specialized instructions
|
|
33
|
+
Use openc0de tools carefully.",
|
|
34
|
+
"type": "text",
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
"tool_choice": {
|
|
38
|
+
"name": "Read",
|
|
39
|
+
"type": "tool",
|
|
40
|
+
},
|
|
41
|
+
"tools": [
|
|
42
|
+
{
|
|
43
|
+
"name": "Read",
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
}
|
|
47
|
+
`);
|
|
47
48
|
});
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
{ ...firstAccount, addedAt: 1, lastUsed: 1 },
|
|
56
|
-
{ ...secondAccount, addedAt: 2, lastUsed: 2 },
|
|
57
|
-
],
|
|
58
|
-
});
|
|
59
|
-
const authSetCalls = [];
|
|
60
|
-
const client = {
|
|
61
|
-
auth: {
|
|
62
|
-
set: async (input) => {
|
|
63
|
-
authSetCalls.push(input);
|
|
49
|
+
test('does not duplicate claude code identity when request was already sanitized', () => {
|
|
50
|
+
const rewritten = rewriteAnthropicRequestPayload(JSON.stringify({
|
|
51
|
+
model: 'claude-sonnet-4-5',
|
|
52
|
+
system: [
|
|
53
|
+
{
|
|
54
|
+
type: 'text',
|
|
55
|
+
text: "You are Claude Code, Anthropic's official CLI for Claude.",
|
|
64
56
|
},
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const store = await loadAccountStore();
|
|
69
|
-
const authJson = JSON.parse(await readFile(authFilePath(), 'utf8'));
|
|
70
|
-
expect(rotated).toMatchObject({
|
|
71
|
-
auth: { refresh: 'refresh-second' },
|
|
72
|
-
fromLabel: '#1 (refresh-...irst)',
|
|
73
|
-
toLabel: '#2 (refresh-...cond)',
|
|
74
|
-
fromIndex: 0,
|
|
75
|
-
toIndex: 1,
|
|
76
|
-
});
|
|
77
|
-
expect(store.activeIndex).toBe(1);
|
|
78
|
-
expect(authJson.anthropic?.refresh).toBe('refresh-second');
|
|
79
|
-
expect(authSetCalls).toEqual([
|
|
80
|
-
{
|
|
81
|
-
path: { id: 'anthropic' },
|
|
82
|
-
body: {
|
|
83
|
-
type: 'oauth',
|
|
84
|
-
refresh: 'refresh-second',
|
|
85
|
-
access: 'access-second',
|
|
86
|
-
expires: 2,
|
|
57
|
+
{
|
|
58
|
+
type: 'text',
|
|
59
|
+
text: '<environment>\n<cwd>/repo</cwd>\n</environment>\nSkills provide specialized instructions',
|
|
87
60
|
},
|
|
88
|
-
},
|
|
89
|
-
]);
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
describe('removeAccount', () => {
|
|
93
|
-
test('removing the active account promotes the next stored account', async () => {
|
|
94
|
-
await saveAccountStore({
|
|
95
|
-
version: 1,
|
|
96
|
-
activeIndex: 1,
|
|
97
|
-
accounts: [
|
|
98
|
-
{ ...firstAccount, addedAt: 1, lastUsed: 1 },
|
|
99
|
-
{ ...secondAccount, addedAt: 2, lastUsed: 2 },
|
|
100
61
|
],
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
await removeAccount(0);
|
|
119
|
-
const store = await loadAccountStore();
|
|
120
|
-
const authJson = JSON.parse(await readFile(authFilePath(), 'utf8'));
|
|
121
|
-
expect(store.accounts).toHaveLength(0);
|
|
122
|
-
expect(authJson.anthropic).toBeUndefined();
|
|
62
|
+
}));
|
|
63
|
+
const payload = parseRewrittenBody(rewritten.body);
|
|
64
|
+
expect(payload.system).toMatchInlineSnapshot(`
|
|
65
|
+
[
|
|
66
|
+
{
|
|
67
|
+
"text": "You are Claude Code, Anthropic's official CLI for Claude.",
|
|
68
|
+
"type": "text",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"text": "<environment>
|
|
72
|
+
<cwd>/repo</cwd>
|
|
73
|
+
</environment>
|
|
74
|
+
Skills provide specialized instructions",
|
|
75
|
+
"type": "text",
|
|
76
|
+
},
|
|
77
|
+
]
|
|
78
|
+
`);
|
|
123
79
|
});
|
|
124
80
|
});
|
|
125
|
-
describe('
|
|
126
|
-
test('
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
81
|
+
describe('replacer', () => {
|
|
82
|
+
test('sanitizes system text only for anthropic provider metadata', async () => {
|
|
83
|
+
const plugin = await replacer({});
|
|
84
|
+
const transform = plugin['experimental.chat.system.transform'];
|
|
85
|
+
if (!transform) {
|
|
86
|
+
throw new Error('Expected experimental.chat.system.transform hook');
|
|
87
|
+
}
|
|
88
|
+
const output = {
|
|
89
|
+
system: [
|
|
90
|
+
"You are OpenCode, the best coding agent on the planet.\nOS: macOS\nSkills provide specialized instructions\nUse opencode tools carefully.",
|
|
91
|
+
],
|
|
92
|
+
};
|
|
93
|
+
await transform({
|
|
94
|
+
model: {
|
|
95
|
+
providerID: 'anthropic',
|
|
96
|
+
},
|
|
97
|
+
}, output);
|
|
98
|
+
expect(output.system).toMatchInlineSnapshot(`
|
|
99
|
+
[
|
|
100
|
+
"
|
|
101
|
+
<environment>
|
|
102
|
+
<cwd>/Users/morse/Documents/GitHub/kimakivoice/cli</cwd>
|
|
103
|
+
</environment>
|
|
104
|
+
Skills provide specialized instructions
|
|
105
|
+
Use openc0de tools carefully.",
|
|
106
|
+
]
|
|
107
|
+
`);
|
|
130
108
|
});
|
|
131
109
|
});
|
package/dist/cli-parsing.test.js
CHANGED
|
@@ -1,84 +1,84 @@
|
|
|
1
1
|
// Regression tests for CLI argument parsing around Discord ID string preservation.
|
|
2
2
|
import { describe, expect, test } from 'vitest';
|
|
3
|
-
import {
|
|
4
|
-
function
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
.option('-c, --channel <channelId>', 'Discord channel ID')
|
|
9
|
-
.
|
|
10
|
-
.option('--
|
|
11
|
-
.option('--
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
.command('
|
|
15
|
-
.
|
|
16
|
-
.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
3
|
+
import { execAsync } from './exec-async.js';
|
|
4
|
+
async function parseWithGoke(argv) {
|
|
5
|
+
const script = [
|
|
6
|
+
"import { goke } from 'goke'",
|
|
7
|
+
'const cli = goke(\'kimaki\')',
|
|
8
|
+
"cli.command('send', 'Send a message').option('-c, --channel <channelId>', 'Discord channel ID').option('--thread <threadId>', 'Thread ID').option('--session <sessionId>', 'Session ID').option('--send-at <schedule>', 'Schedule')",
|
|
9
|
+
"cli.command('session archive <threadId>', 'Archive a thread')",
|
|
10
|
+
"cli.command('session search <query>', 'Search sessions').option('--channel <channelId>', 'Discord channel ID').option('--project <path>', 'Project path')",
|
|
11
|
+
"cli.command('session export-events-jsonl', 'Export in-memory events to JSONL').option('--session <sessionId>', 'Session ID').option('--out <file>', 'Output path')",
|
|
12
|
+
"cli.command('add-project', 'Add a project').option('-g, --guild <guildId>', 'Discord guild/server ID')",
|
|
13
|
+
"cli.command('task delete <id>', 'Delete task')",
|
|
14
|
+
"cli.command('anthropic-accounts list', 'List stored Anthropic accounts')",
|
|
15
|
+
"cli.command('anthropic-accounts remove <indexOrEmail>', 'Remove stored Anthropic account')",
|
|
16
|
+
`const result = cli.parse(${JSON.stringify(argv)}, { run: false })`,
|
|
17
|
+
'process.stdout.write(JSON.stringify({ args: result.args, options: result.options }))',
|
|
18
|
+
].join(';');
|
|
19
|
+
const { stdout } = await execAsync(`node --input-type=module -e ${JSON.stringify(script)}`, {
|
|
20
|
+
cwd: import.meta.dirname,
|
|
21
|
+
timeout: 10_000,
|
|
22
|
+
});
|
|
23
|
+
return JSON.parse(stdout);
|
|
24
|
+
}
|
|
25
|
+
async function getHelpOutput() {
|
|
26
|
+
const script = [
|
|
27
|
+
"import { goke } from 'goke'",
|
|
28
|
+
'const stdout = { text: \'\', write(data) { this.text += String(data) } }',
|
|
29
|
+
"const cli = goke('kimaki', { stdout })",
|
|
30
|
+
"cli.command('send', 'Send a message')",
|
|
31
|
+
"cli.command('anthropic-accounts list', 'List stored Anthropic accounts')",
|
|
32
|
+
'cli.help()',
|
|
33
|
+
"cli.parse(['node', 'kimaki', '--help'], { run: false })",
|
|
34
|
+
'process.stdout.write(stdout.text)',
|
|
35
|
+
].join(';');
|
|
36
|
+
const { stdout } = await execAsync(`node --input-type=module -e ${JSON.stringify(script)}`, {
|
|
37
|
+
cwd: import.meta.dirname,
|
|
38
|
+
timeout: 10_000,
|
|
39
|
+
});
|
|
40
|
+
return stdout;
|
|
28
41
|
}
|
|
29
42
|
describe('goke CLI ID parsing', () => {
|
|
30
|
-
test('keeps large Discord IDs as strings', () => {
|
|
31
|
-
const cli = createCliForIdParsing();
|
|
43
|
+
test('keeps large Discord IDs as strings', async () => {
|
|
32
44
|
const channelId = '1234567890123456789';
|
|
33
45
|
const threadId = '9876543210987654321';
|
|
34
46
|
const sessionId = '1111222233334444555';
|
|
35
|
-
const channelResult =
|
|
36
|
-
run: false,
|
|
37
|
-
});
|
|
47
|
+
const channelResult = await parseWithGoke(['node', 'kimaki', 'send', '--channel', channelId]);
|
|
38
48
|
expect(channelResult.options.channel).toBe(channelId);
|
|
39
49
|
expect(typeof channelResult.options.channel).toBe('string');
|
|
40
|
-
const threadResult =
|
|
50
|
+
const threadResult = await parseWithGoke(['node', 'kimaki', 'send', '--thread', threadId]);
|
|
41
51
|
expect(threadResult.options.thread).toBe(threadId);
|
|
42
52
|
expect(typeof threadResult.options.thread).toBe('string');
|
|
43
|
-
const sessionResult =
|
|
44
|
-
run: false,
|
|
45
|
-
});
|
|
53
|
+
const sessionResult = await parseWithGoke(['node', 'kimaki', 'send', '--session', sessionId]);
|
|
46
54
|
expect(sessionResult.options.session).toBe(sessionId);
|
|
47
55
|
expect(typeof sessionResult.options.session).toBe('string');
|
|
48
56
|
});
|
|
49
|
-
test('preserves leading zeros in Discord IDs', () => {
|
|
50
|
-
const cli = createCliForIdParsing();
|
|
57
|
+
test('preserves leading zeros in Discord IDs', async () => {
|
|
51
58
|
const guildId = '001230045600789';
|
|
52
|
-
const result =
|
|
59
|
+
const result = await parseWithGoke(['node', 'kimaki', 'add-project', '--guild', guildId]);
|
|
53
60
|
expect(result.options.guild).toBe(guildId);
|
|
54
61
|
expect(typeof result.options.guild).toBe('string');
|
|
55
62
|
});
|
|
56
|
-
test('keeps session archive thread ID as string', () => {
|
|
57
|
-
const cli = createCliForIdParsing();
|
|
63
|
+
test('keeps session archive thread ID as string', async () => {
|
|
58
64
|
const threadId = '0098765432109876543';
|
|
59
|
-
const result =
|
|
60
|
-
run: false,
|
|
61
|
-
});
|
|
65
|
+
const result = await parseWithGoke(['node', 'kimaki', 'session', 'archive', threadId]);
|
|
62
66
|
expect(result.args[0]).toBe(threadId);
|
|
63
67
|
expect(typeof result.args[0]).toBe('string');
|
|
64
68
|
});
|
|
65
|
-
test('keeps session search regex and channel ID as strings', () => {
|
|
66
|
-
const cli = createCliForIdParsing();
|
|
69
|
+
test('keeps session search regex and channel ID as strings', async () => {
|
|
67
70
|
const channelId = '0012345678901234567';
|
|
68
71
|
const query = '/error\\s+42/i';
|
|
69
|
-
const result =
|
|
70
|
-
run: false,
|
|
71
|
-
});
|
|
72
|
+
const result = await parseWithGoke(['node', 'kimaki', 'session', 'search', query, '--channel', channelId]);
|
|
72
73
|
expect(result.args[0]).toBe(query);
|
|
73
74
|
expect(typeof result.args[0]).toBe('string');
|
|
74
75
|
expect(result.options.channel).toBe(channelId);
|
|
75
76
|
expect(typeof result.options.channel).toBe('string');
|
|
76
77
|
});
|
|
77
|
-
test('keeps session export options as strings', () => {
|
|
78
|
-
const cli = createCliForIdParsing();
|
|
78
|
+
test('keeps session export options as strings', async () => {
|
|
79
79
|
const sessionId = '001111222233334444';
|
|
80
80
|
const outPath = './tmp/session-events.jsonl';
|
|
81
|
-
const result =
|
|
81
|
+
const result = await parseWithGoke([
|
|
82
82
|
'node',
|
|
83
83
|
'kimaki',
|
|
84
84
|
'session',
|
|
@@ -87,54 +87,35 @@ describe('goke CLI ID parsing', () => {
|
|
|
87
87
|
sessionId,
|
|
88
88
|
'--out',
|
|
89
89
|
outPath,
|
|
90
|
-
]
|
|
91
|
-
run: false,
|
|
92
|
-
});
|
|
90
|
+
]);
|
|
93
91
|
expect(result.options.session).toBe(sessionId);
|
|
94
92
|
expect(typeof result.options.session).toBe('string');
|
|
95
93
|
expect(result.options.out).toBe(outPath);
|
|
96
94
|
expect(typeof result.options.out).toBe('string');
|
|
97
95
|
});
|
|
98
|
-
test('keeps --send-at cron string intact', () => {
|
|
99
|
-
const cli = createCliForIdParsing();
|
|
96
|
+
test('keeps --send-at cron string intact', async () => {
|
|
100
97
|
const cron = '0 9 * * 1';
|
|
101
|
-
const result =
|
|
102
|
-
run: false,
|
|
103
|
-
});
|
|
98
|
+
const result = await parseWithGoke(['node', 'kimaki', 'send', '--send-at', cron]);
|
|
104
99
|
expect(result.options.sendAt).toBe(cron);
|
|
105
100
|
expect(typeof result.options.sendAt).toBe('string');
|
|
106
101
|
});
|
|
107
|
-
test('keeps task delete ID as string before validation', () => {
|
|
108
|
-
const cli = createCliForIdParsing();
|
|
102
|
+
test('keeps task delete ID as string before validation', async () => {
|
|
109
103
|
const taskId = '0012345';
|
|
110
|
-
const result =
|
|
111
|
-
run: false,
|
|
112
|
-
});
|
|
104
|
+
const result = await parseWithGoke(['node', 'kimaki', 'task', 'delete', taskId]);
|
|
113
105
|
expect(result.args[0]).toBe(taskId);
|
|
114
106
|
expect(typeof result.args[0]).toBe('string');
|
|
115
107
|
});
|
|
116
|
-
test('anthropic account remove parses index and email as strings', () => {
|
|
117
|
-
const
|
|
118
|
-
const
|
|
119
|
-
const emailResult = cli.parse(['node', 'kimaki', 'anthropic-accounts', 'remove', 'user@example.com'], { run: false });
|
|
108
|
+
test('anthropic account remove parses index and email as strings', async () => {
|
|
109
|
+
const indexResult = await parseWithGoke(['node', 'kimaki', 'anthropic-accounts', 'remove', '2']);
|
|
110
|
+
const emailResult = await parseWithGoke(['node', 'kimaki', 'anthropic-accounts', 'remove', 'user@example.com']);
|
|
120
111
|
expect(indexResult.args[0]).toBe('2');
|
|
121
112
|
expect(typeof indexResult.args[0]).toBe('string');
|
|
122
113
|
expect(emailResult.args[0]).toBe('user@example.com');
|
|
123
114
|
expect(typeof emailResult.args[0]).toBe('string');
|
|
124
115
|
});
|
|
125
|
-
test('anthropic account commands are included in help output', () => {
|
|
126
|
-
const stdout =
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
this.text += String(data);
|
|
130
|
-
},
|
|
131
|
-
};
|
|
132
|
-
const cli = goke('kimaki', { stdout: stdout });
|
|
133
|
-
cli.command('send', 'Send a message');
|
|
134
|
-
cli.command('anthropic-accounts list', 'List stored Anthropic accounts');
|
|
135
|
-
cli.help();
|
|
136
|
-
cli.parse(['node', 'kimaki', '--help'], { run: false });
|
|
137
|
-
expect(stdout.text).toContain('send');
|
|
138
|
-
expect(stdout.text).toContain('anthropic-accounts');
|
|
116
|
+
test('anthropic account commands are included in help output', async () => {
|
|
117
|
+
const stdout = await getHelpOutput();
|
|
118
|
+
expect(stdout).toContain('send');
|
|
119
|
+
expect(stdout).toContain('anthropic-accounts');
|
|
139
120
|
});
|
|
140
121
|
});
|
|
@@ -182,7 +182,7 @@ describe('kimaki send --channel thread creation', () => {
|
|
|
182
182
|
if (warmup instanceof Error) {
|
|
183
183
|
throw warmup;
|
|
184
184
|
}
|
|
185
|
-
},
|
|
185
|
+
}, 20_000);
|
|
186
186
|
afterAll(async () => {
|
|
187
187
|
if (directories) {
|
|
188
188
|
await cleanupTestSessions({
|
|
@@ -213,7 +213,7 @@ describe('kimaki send --channel thread creation', () => {
|
|
|
213
213
|
if (directories) {
|
|
214
214
|
fs.rmSync(directories.dataDir, { recursive: true, force: true });
|
|
215
215
|
}
|
|
216
|
-
},
|
|
216
|
+
}, 5_000);
|
|
217
217
|
test('kimaki send --prompt "/hello-test-cmd" falls through as text when registeredUserCommands is empty (repro #97)', async () => {
|
|
218
218
|
// Reproduce GitHub #97: when registeredUserCommands is empty (gateway mode
|
|
219
219
|
// startup race, or backgroundInit not complete), the prompt "/hello-test-cmd"
|