@otto-assistant/bridge 0.4.101 → 0.4.103
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 +1 -0
- package/dist/anthropic-auth-plugin.js +22 -1
- package/dist/anthropic-auth-state.js +31 -0
- package/dist/btw-prefix-detection.js +17 -0
- package/dist/btw-prefix-detection.test.js +63 -0
- package/dist/cli.js +101 -15
- package/dist/commands/agent.js +21 -2
- package/dist/commands/ask-question.js +50 -4
- package/dist/commands/ask-question.test.js +92 -0
- package/dist/commands/btw.js +71 -66
- package/dist/commands/new-worktree.js +92 -35
- package/dist/commands/queue.js +17 -0
- package/dist/commands/worktrees.js +196 -139
- package/dist/context-awareness-plugin.js +16 -8
- package/dist/context-awareness-plugin.test.js +4 -2
- package/dist/discord-bot.js +35 -2
- package/dist/discord-command-registration.js +9 -2
- package/dist/memory-overview-plugin.js +3 -1
- package/dist/opencode.js +24 -1
- package/dist/queue-question-select-drain.e2e.test.js +135 -10
- package/dist/session-handler/thread-runtime-state.js +27 -0
- package/dist/session-handler/thread-session-runtime.js +58 -28
- package/dist/session-title-rename.test.js +12 -0
- package/dist/skill-filter.js +31 -0
- package/dist/skill-filter.test.js +65 -0
- package/dist/store.js +2 -0
- package/dist/system-message.js +12 -3
- package/dist/system-message.test.js +10 -6
- package/dist/thread-message-queue.e2e.test.js +109 -0
- package/dist/worktree-lifecycle.e2e.test.js +4 -1
- package/dist/worktrees.js +106 -12
- package/dist/worktrees.test.js +232 -6
- package/package.json +2 -2
- package/skills/goke/SKILL.md +13 -619
- package/skills/new-skill/SKILL.md +34 -10
- package/skills/npm-package/SKILL.md +336 -2
- package/skills/profano/SKILL.md +24 -0
- package/skills/zele/SKILL.md +50 -21
- package/src/agent-model.e2e.test.ts +1 -0
- package/src/anthropic-auth-plugin.ts +24 -4
- package/src/anthropic-auth-state.ts +45 -0
- package/src/btw-prefix-detection.test.ts +73 -0
- package/src/btw-prefix-detection.ts +23 -0
- package/src/cli.ts +138 -46
- package/src/commands/agent.ts +24 -2
- package/src/commands/ask-question.test.ts +111 -0
- package/src/commands/ask-question.ts +69 -4
- package/src/commands/btw.ts +105 -85
- package/src/commands/new-worktree.ts +107 -40
- package/src/commands/queue.ts +22 -0
- package/src/commands/worktrees.ts +246 -154
- package/src/context-awareness-plugin.test.ts +4 -2
- package/src/context-awareness-plugin.ts +16 -8
- package/src/discord-bot.ts +40 -2
- package/src/discord-command-registration.ts +12 -2
- package/src/memory-overview-plugin.ts +3 -1
- package/src/opencode.ts +31 -1
- package/src/queue-question-select-drain.e2e.test.ts +174 -10
- package/src/session-handler/thread-runtime-state.ts +36 -1
- package/src/session-handler/thread-session-runtime.ts +72 -32
- package/src/session-title-rename.test.ts +18 -0
- package/src/skill-filter.test.ts +83 -0
- package/src/skill-filter.ts +42 -0
- package/src/store.ts +17 -0
- package/src/system-message.test.ts +10 -6
- package/src/system-message.ts +12 -3
- package/src/thread-message-queue.e2e.test.ts +126 -0
- package/src/worktree-lifecycle.e2e.test.ts +6 -1
- package/src/worktrees.test.ts +274 -9
- package/src/worktrees.ts +144 -23
|
@@ -17,6 +17,12 @@ export type OAuthStored = {
|
|
|
17
17
|
expires: number
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
export type CurrentAnthropicAccount = {
|
|
21
|
+
auth: OAuthStored
|
|
22
|
+
account?: OAuthStored & AnthropicAccountIdentity
|
|
23
|
+
index?: number
|
|
24
|
+
}
|
|
25
|
+
|
|
20
26
|
type AccountRecord = OAuthStored & {
|
|
21
27
|
email?: string
|
|
22
28
|
accountId?: string
|
|
@@ -243,6 +249,45 @@ async function writeAnthropicAuthFile(auth: OAuthStored | undefined) {
|
|
|
243
249
|
await writeJson(file, data)
|
|
244
250
|
}
|
|
245
251
|
|
|
252
|
+
function isOAuthStored(value: unknown): value is OAuthStored {
|
|
253
|
+
if (!value || typeof value !== 'object') {
|
|
254
|
+
return false
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const record = value as Record<string, unknown>
|
|
258
|
+
return (
|
|
259
|
+
record.type === 'oauth' &&
|
|
260
|
+
typeof record.refresh === 'string' &&
|
|
261
|
+
typeof record.access === 'string' &&
|
|
262
|
+
typeof record.expires === 'number'
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export async function getCurrentAnthropicAccount() {
|
|
267
|
+
const authJson = await readJson<Record<string, unknown>>(authFilePath(), {})
|
|
268
|
+
const auth = authJson.anthropic
|
|
269
|
+
if (!isOAuthStored(auth)) {
|
|
270
|
+
return null
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const store = await loadAccountStore()
|
|
274
|
+
const index = findCurrentAccountIndex(store, auth)
|
|
275
|
+
const account = store.accounts[index]
|
|
276
|
+
if (!account) {
|
|
277
|
+
return { auth } satisfies CurrentAnthropicAccount
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (account.refresh !== auth.refresh && account.access !== auth.access) {
|
|
281
|
+
return { auth } satisfies CurrentAnthropicAccount
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
auth,
|
|
286
|
+
account,
|
|
287
|
+
index,
|
|
288
|
+
} satisfies CurrentAnthropicAccount
|
|
289
|
+
}
|
|
290
|
+
|
|
246
291
|
export async function setAnthropicAuth(
|
|
247
292
|
auth: OAuthStored,
|
|
248
293
|
client: Parameters<Plugin>[0]['client'],
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest'
|
|
2
|
+
import { extractBtwPrefix } from './btw-prefix-detection.js'
|
|
3
|
+
|
|
4
|
+
describe('extractBtwPrefix', () => {
|
|
5
|
+
test('matches lowercase prefix', () => {
|
|
6
|
+
expect(extractBtwPrefix('btw fix this')).toMatchInlineSnapshot(`
|
|
7
|
+
{
|
|
8
|
+
"prompt": "fix this",
|
|
9
|
+
}
|
|
10
|
+
`)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
test('matches uppercase prefix', () => {
|
|
14
|
+
expect(extractBtwPrefix('BTW check this')).toMatchInlineSnapshot(`
|
|
15
|
+
{
|
|
16
|
+
"prompt": "check this",
|
|
17
|
+
}
|
|
18
|
+
`)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('keeps multiline content', () => {
|
|
22
|
+
expect(extractBtwPrefix(' btw first line\nsecond line ')).toMatchInlineSnapshot(`
|
|
23
|
+
{
|
|
24
|
+
"prompt": "first line
|
|
25
|
+
second line",
|
|
26
|
+
}
|
|
27
|
+
`)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('matches dot separator', () => {
|
|
31
|
+
expect(extractBtwPrefix('btw. fix this')).toMatchInlineSnapshot(`
|
|
32
|
+
{
|
|
33
|
+
"prompt": "fix this",
|
|
34
|
+
}
|
|
35
|
+
`)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('matches comma separator', () => {
|
|
39
|
+
expect(extractBtwPrefix('btw, fix this')).toMatchInlineSnapshot(`
|
|
40
|
+
{
|
|
41
|
+
"prompt": "fix this",
|
|
42
|
+
}
|
|
43
|
+
`)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('matches colon separator', () => {
|
|
47
|
+
expect(extractBtwPrefix('btw: fix this')).toMatchInlineSnapshot(`
|
|
48
|
+
{
|
|
49
|
+
"prompt": "fix this",
|
|
50
|
+
}
|
|
51
|
+
`)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('matches punctuation without trailing space', () => {
|
|
55
|
+
expect(extractBtwPrefix('btw.fix this')).toMatchInlineSnapshot(`
|
|
56
|
+
{
|
|
57
|
+
"prompt": "fix this",
|
|
58
|
+
}
|
|
59
|
+
`)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('does not match without separating whitespace', () => {
|
|
63
|
+
expect(extractBtwPrefix('btwfix this')).toMatchInlineSnapshot(`null`)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('does not match mid-message', () => {
|
|
67
|
+
expect(extractBtwPrefix('hello btw fix this')).toMatchInlineSnapshot(`null`)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('does not match empty payload', () => {
|
|
71
|
+
expect(extractBtwPrefix('btw ')).toMatchInlineSnapshot(`null`)
|
|
72
|
+
})
|
|
73
|
+
})
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Detects the raw `btw ` Discord message shortcut used to fork a side-question
|
|
2
|
+
// thread without invoking the /btw slash command UI.
|
|
3
|
+
|
|
4
|
+
export function extractBtwPrefix(
|
|
5
|
+
content: string,
|
|
6
|
+
): { prompt: string } | null {
|
|
7
|
+
if (!content) {
|
|
8
|
+
return null
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Match "btw" followed by whitespace or punctuation (. , : ; ! ?) then the prompt
|
|
12
|
+
const match = content.match(/^\s*btw[.,;:!?\s]\s*([\s\S]+)$/i)
|
|
13
|
+
if (!match) {
|
|
14
|
+
return null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const prompt = match[1]?.trim()
|
|
18
|
+
if (!prompt) {
|
|
19
|
+
return null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return { prompt }
|
|
23
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -64,7 +64,7 @@ import {
|
|
|
64
64
|
buildSessionSearchSnippet,
|
|
65
65
|
getPartSearchTexts,
|
|
66
66
|
} from './session-search.js'
|
|
67
|
-
import { formatWorktreeName } from './commands/new-worktree.js'
|
|
67
|
+
import { formatWorktreeName, formatAutoWorktreeName } from './commands/new-worktree.js'
|
|
68
68
|
import { WORKTREE_PREFIX } from './commands/merge-worktree.js'
|
|
69
69
|
import type { ThreadStartMarker } from './system-message.js'
|
|
70
70
|
import { sendWelcomeMessage } from './onboarding-welcome.js'
|
|
@@ -90,6 +90,7 @@ import { createDiscordRest, discordApiUrl, getDiscordRestApiUrl, getGatewayProxy
|
|
|
90
90
|
import crypto from 'node:crypto'
|
|
91
91
|
import path from 'node:path'
|
|
92
92
|
import fs from 'node:fs'
|
|
93
|
+
import { fileURLToPath } from 'node:url'
|
|
93
94
|
import * as errore from 'errore'
|
|
94
95
|
|
|
95
96
|
import { createLogger, formatErrorWithStack, initLogFile, LogPrefix } from './logger.js'
|
|
@@ -127,6 +128,8 @@ import {
|
|
|
127
128
|
import {
|
|
128
129
|
accountLabel,
|
|
129
130
|
accountsFilePath,
|
|
131
|
+
authFilePath,
|
|
132
|
+
getCurrentAnthropicAccount,
|
|
130
133
|
loadAccountStore,
|
|
131
134
|
removeAccount,
|
|
132
135
|
} from './anthropic-auth-state.js'
|
|
@@ -1840,6 +1843,24 @@ cli
|
|
|
1840
1843
|
'--gateway-callback-url <url>',
|
|
1841
1844
|
'After gateway OAuth install, redirect to this URL instead of the default success page (appends ?guild_id=<id>)',
|
|
1842
1845
|
)
|
|
1846
|
+
.option(
|
|
1847
|
+
'--enable-skill <name>',
|
|
1848
|
+
z
|
|
1849
|
+
.array(z.string())
|
|
1850
|
+
.optional()
|
|
1851
|
+
.describe(
|
|
1852
|
+
'Whitelist a built-in skill by name. Only the listed skills are injected into the model (all others are hidden via an opencode permission.skill deny-all rule). Repeatable: pass --enable-skill multiple times. Mutually exclusive with --disable-skill. See https://github.com/remorses/kimaki/tree/main/cli/skills for available skills.',
|
|
1853
|
+
),
|
|
1854
|
+
)
|
|
1855
|
+
.option(
|
|
1856
|
+
'--disable-skill <name>',
|
|
1857
|
+
z
|
|
1858
|
+
.array(z.string())
|
|
1859
|
+
.optional()
|
|
1860
|
+
.describe(
|
|
1861
|
+
'Blacklist a built-in skill by name. Listed skills are hidden from the model. Repeatable: pass --disable-skill multiple times. Mutually exclusive with --enable-skill. See https://github.com/remorses/kimaki/tree/main/cli/skills for available skills.',
|
|
1862
|
+
),
|
|
1863
|
+
)
|
|
1843
1864
|
.action(
|
|
1844
1865
|
async (options: {
|
|
1845
1866
|
restartOnboarding?: boolean
|
|
@@ -1856,6 +1877,8 @@ cli
|
|
|
1856
1877
|
noSentry?: boolean
|
|
1857
1878
|
gateway?: boolean
|
|
1858
1879
|
gatewayCallbackUrl?: string
|
|
1880
|
+
enableSkill?: string[]
|
|
1881
|
+
disableSkill?: string[]
|
|
1859
1882
|
}) => {
|
|
1860
1883
|
// Guard: only one kimaki bot process can run at a time (they share a lock
|
|
1861
1884
|
// port). Running `kimaki` here would kill the already-running bot process
|
|
@@ -1899,6 +1922,47 @@ cli
|
|
|
1899
1922
|
}
|
|
1900
1923
|
}
|
|
1901
1924
|
|
|
1925
|
+
// --enable-skill and --disable-skill are mutually exclusive: the user
|
|
1926
|
+
// either whitelists a small allowlist or blacklists a few unwanted
|
|
1927
|
+
// skills, never both. Applied later in opencode.ts as permission.skill
|
|
1928
|
+
// rules via computeSkillPermission().
|
|
1929
|
+
const enabledSkills = options.enableSkill ?? []
|
|
1930
|
+
const disabledSkills = options.disableSkill ?? []
|
|
1931
|
+
if (enabledSkills.length > 0 && disabledSkills.length > 0) {
|
|
1932
|
+
cliLogger.error(
|
|
1933
|
+
'Cannot use --enable-skill and --disable-skill at the same time. Use one or the other.',
|
|
1934
|
+
)
|
|
1935
|
+
process.exit(EXIT_NO_RESTART)
|
|
1936
|
+
}
|
|
1937
|
+
// Soft-validate skill names against the bundled skills/ folder. Users
|
|
1938
|
+
// may rely on skills loaded from their own .opencode / .claude / .agents
|
|
1939
|
+
// dirs, so unknown names only emit a warning rather than hard-failing.
|
|
1940
|
+
if (enabledSkills.length > 0 || disabledSkills.length > 0) {
|
|
1941
|
+
const bundledSkillsDir = path.resolve(
|
|
1942
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
1943
|
+
'..',
|
|
1944
|
+
'skills',
|
|
1945
|
+
)
|
|
1946
|
+
const availableBundledSkills = (() => {
|
|
1947
|
+
try {
|
|
1948
|
+
return fs
|
|
1949
|
+
.readdirSync(bundledSkillsDir, { withFileTypes: true })
|
|
1950
|
+
.filter((entry) => entry.isDirectory())
|
|
1951
|
+
.map((entry) => entry.name)
|
|
1952
|
+
} catch {
|
|
1953
|
+
return [] as string[]
|
|
1954
|
+
}
|
|
1955
|
+
})()
|
|
1956
|
+
const availableSet = new Set(availableBundledSkills)
|
|
1957
|
+
for (const name of [...enabledSkills, ...disabledSkills]) {
|
|
1958
|
+
if (!availableSet.has(name)) {
|
|
1959
|
+
cliLogger.warn(
|
|
1960
|
+
`Skill "${name}" is not a bundled kimaki skill. Rule will still apply (user-provided skills from .opencode/.claude/.agents dirs may match). Available bundled skills: ${availableBundledSkills.join(', ')}`,
|
|
1961
|
+
)
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1902
1966
|
store.setState({
|
|
1903
1967
|
...(options.verbosity && {
|
|
1904
1968
|
defaultVerbosity: options.verbosity as
|
|
@@ -1908,8 +1972,21 @@ cli
|
|
|
1908
1972
|
}),
|
|
1909
1973
|
...(options.mentionMode && { defaultMentionMode: true }),
|
|
1910
1974
|
...(options.noCritique && { critiqueEnabled: false }),
|
|
1975
|
+
...(enabledSkills.length > 0 && { enabledSkills }),
|
|
1976
|
+
...(disabledSkills.length > 0 && { disabledSkills }),
|
|
1911
1977
|
})
|
|
1912
1978
|
|
|
1979
|
+
if (enabledSkills.length > 0) {
|
|
1980
|
+
cliLogger.log(
|
|
1981
|
+
`Skill whitelist enabled: only [${enabledSkills.join(', ')}] will be injected`,
|
|
1982
|
+
)
|
|
1983
|
+
}
|
|
1984
|
+
if (disabledSkills.length > 0) {
|
|
1985
|
+
cliLogger.log(
|
|
1986
|
+
`Skill blacklist enabled: [${disabledSkills.join(', ')}] will be hidden`,
|
|
1987
|
+
)
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1913
1990
|
if (options.verbosity) {
|
|
1914
1991
|
cliLogger.log(`Default verbosity: ${options.verbosity}`)
|
|
1915
1992
|
}
|
|
@@ -2382,36 +2459,20 @@ cli
|
|
|
2382
2459
|
'--wait',
|
|
2383
2460
|
'Wait for session to complete, then print session text to stdout',
|
|
2384
2461
|
)
|
|
2385
|
-
.action(
|
|
2386
|
-
async (options: {
|
|
2387
|
-
channel?: string
|
|
2388
|
-
project?: string
|
|
2389
|
-
prompt?: string
|
|
2390
|
-
name?: string
|
|
2391
|
-
appId?: string
|
|
2392
|
-
notifyOnly?: boolean
|
|
2393
|
-
worktree?: string | boolean
|
|
2394
|
-
cwd?: string
|
|
2395
|
-
user?: string
|
|
2396
|
-
agent?: string
|
|
2397
|
-
model?: string
|
|
2398
|
-
permission?: string[]
|
|
2399
|
-
injectionGuard?: string[]
|
|
2400
|
-
sendAt?: string
|
|
2401
|
-
thread?: string
|
|
2402
|
-
session?: string
|
|
2403
|
-
wait?: boolean
|
|
2404
|
-
}) => {
|
|
2462
|
+
.action(async (options) => {
|
|
2405
2463
|
try {
|
|
2464
|
+
// `--name` / `--app-id` are optional-value flags: `undefined` when
|
|
2465
|
+
// omitted, `''` when passed bare, a real string when given a value.
|
|
2466
|
+
// `||` collapses `''` to `undefined` for downstream consumers.
|
|
2467
|
+
const optionAppId = options.appId || undefined
|
|
2406
2468
|
let {
|
|
2407
2469
|
channel: channelId,
|
|
2408
2470
|
prompt,
|
|
2409
|
-
name,
|
|
2410
|
-
appId: optionAppId,
|
|
2411
2471
|
notifyOnly,
|
|
2412
2472
|
thread: threadId,
|
|
2413
2473
|
session: sessionId,
|
|
2414
2474
|
} = options
|
|
2475
|
+
let name: string | undefined = options.name || undefined
|
|
2415
2476
|
const { project: projectPath } = options
|
|
2416
2477
|
const sendAt = options.sendAt
|
|
2417
2478
|
|
|
@@ -2862,12 +2923,12 @@ cli
|
|
|
2862
2923
|
(cleanPrompt.length > 80
|
|
2863
2924
|
? cleanPrompt.slice(0, 77) + '...'
|
|
2864
2925
|
: cleanPrompt)
|
|
2926
|
+
// Explicit string => use as-is via formatWorktreeName (no vowel strip).
|
|
2927
|
+
// Boolean true => derived from thread/prompt, compress via formatAutoWorktreeName.
|
|
2865
2928
|
const worktreeName = options.worktree
|
|
2866
|
-
?
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
: baseThreadName,
|
|
2870
|
-
)
|
|
2929
|
+
? typeof options.worktree === 'string'
|
|
2930
|
+
? formatWorktreeName(options.worktree)
|
|
2931
|
+
: formatAutoWorktreeName(baseThreadName)
|
|
2871
2932
|
: undefined
|
|
2872
2933
|
const threadName = worktreeName
|
|
2873
2934
|
? `${WORKTREE_PREFIX}${baseThreadName}`
|
|
@@ -3185,6 +3246,42 @@ cli
|
|
|
3185
3246
|
process.exit(0)
|
|
3186
3247
|
})
|
|
3187
3248
|
|
|
3249
|
+
cli
|
|
3250
|
+
.command(
|
|
3251
|
+
'anthropic-accounts current',
|
|
3252
|
+
'Show the current Anthropic OAuth account being used, if any',
|
|
3253
|
+
)
|
|
3254
|
+
.action(async () => {
|
|
3255
|
+
const current = await getCurrentAnthropicAccount()
|
|
3256
|
+
console.log(`Store: ${accountsFilePath()}`)
|
|
3257
|
+
console.log(`Auth: ${authFilePath()}`)
|
|
3258
|
+
|
|
3259
|
+
if (!current) {
|
|
3260
|
+
console.log('No active Anthropic OAuth account configured.')
|
|
3261
|
+
process.exit(0)
|
|
3262
|
+
}
|
|
3263
|
+
|
|
3264
|
+
const lines: string[] = []
|
|
3265
|
+
lines.push(`Current: ${accountLabel(current.account || current.auth, current.index)}`)
|
|
3266
|
+
|
|
3267
|
+
if (current.account?.email) {
|
|
3268
|
+
lines.push(`Email: ${current.account.email}`)
|
|
3269
|
+
} else {
|
|
3270
|
+
lines.push('Email: unavailable')
|
|
3271
|
+
}
|
|
3272
|
+
|
|
3273
|
+
if (current.account?.accountId) {
|
|
3274
|
+
lines.push(`Account ID: ${current.account.accountId}`)
|
|
3275
|
+
}
|
|
3276
|
+
|
|
3277
|
+
if (!current.account) {
|
|
3278
|
+
lines.push('Rotation pool entry: not found')
|
|
3279
|
+
}
|
|
3280
|
+
|
|
3281
|
+
console.log(lines.join('\n'))
|
|
3282
|
+
process.exit(0)
|
|
3283
|
+
})
|
|
3284
|
+
|
|
3188
3285
|
cli
|
|
3189
3286
|
.command(
|
|
3190
3287
|
'anthropic-accounts remove <indexOrEmail>',
|
|
@@ -3693,13 +3790,15 @@ cli
|
|
|
3693
3790
|
)
|
|
3694
3791
|
.option('-g, --guild <guildId>', 'Discord guild/server ID (required)')
|
|
3695
3792
|
.option('-q, --query [query]', 'Search query to filter users by name')
|
|
3696
|
-
.action(async (options
|
|
3793
|
+
.action(async (options) => {
|
|
3697
3794
|
try {
|
|
3698
3795
|
if (!options.guild) {
|
|
3699
3796
|
cliLogger.error('Guild ID is required. Use --guild <guildId>')
|
|
3700
3797
|
process.exit(EXIT_NO_RESTART)
|
|
3701
3798
|
}
|
|
3702
3799
|
const guildId = String(options.guild)
|
|
3800
|
+
// Bare `--query` comes through as `''`; collapse it to undefined
|
|
3801
|
+
const query = options.query || undefined
|
|
3703
3802
|
|
|
3704
3803
|
await initDatabase()
|
|
3705
3804
|
const { token: botToken } = await resolveBotCredentials()
|
|
@@ -3711,9 +3810,9 @@ cli
|
|
|
3711
3810
|
}
|
|
3712
3811
|
|
|
3713
3812
|
const members: GuildMember[] = await (async () => {
|
|
3714
|
-
if (
|
|
3813
|
+
if (query) {
|
|
3715
3814
|
return (await rest.get(Routes.guildMembersSearch(guildId), {
|
|
3716
|
-
query: new URLSearchParams({ query
|
|
3815
|
+
query: new URLSearchParams({ query, limit: '20' }),
|
|
3717
3816
|
})) as GuildMember[]
|
|
3718
3817
|
}
|
|
3719
3818
|
return (await rest.get(Routes.guildMembers(guildId), {
|
|
@@ -3722,8 +3821,8 @@ cli
|
|
|
3722
3821
|
})()
|
|
3723
3822
|
|
|
3724
3823
|
if (members.length === 0) {
|
|
3725
|
-
const msg =
|
|
3726
|
-
? `No users found matching "${
|
|
3824
|
+
const msg = query
|
|
3825
|
+
? `No users found matching "${query}"`
|
|
3727
3826
|
: 'No users found in guild'
|
|
3728
3827
|
cliLogger.log(msg)
|
|
3729
3828
|
process.exit(0)
|
|
@@ -3736,8 +3835,8 @@ cli
|
|
|
3736
3835
|
})
|
|
3737
3836
|
.join('\n')
|
|
3738
3837
|
|
|
3739
|
-
const header =
|
|
3740
|
-
? `Found ${members.length} users matching "${
|
|
3838
|
+
const header = query
|
|
3839
|
+
? `Found ${members.length} users matching "${query}":`
|
|
3741
3840
|
: `Found ${members.length} users:`
|
|
3742
3841
|
|
|
3743
3842
|
console.log(`${header}\n${userList}`)
|
|
@@ -3761,14 +3860,7 @@ cli
|
|
|
3761
3860
|
.option('-h, --host [host]', 'Local host (default: localhost)')
|
|
3762
3861
|
.option('-s, --server [url]', 'Tunnel server URL')
|
|
3763
3862
|
.option('-k, --kill', 'Kill any existing process on the port before starting')
|
|
3764
|
-
.action(
|
|
3765
|
-
async (options: {
|
|
3766
|
-
port?: string
|
|
3767
|
-
tunnelId?: string
|
|
3768
|
-
host?: string
|
|
3769
|
-
server?: string
|
|
3770
|
-
kill?: boolean
|
|
3771
|
-
}) => {
|
|
3863
|
+
.action(async (options) => {
|
|
3772
3864
|
const { runTunnel, parseCommandFromArgv, CLI_NAME } = await import(
|
|
3773
3865
|
'traforo/run-tunnel'
|
|
3774
3866
|
)
|
|
@@ -3790,10 +3882,10 @@ cli
|
|
|
3790
3882
|
|
|
3791
3883
|
await runTunnel({
|
|
3792
3884
|
port,
|
|
3793
|
-
tunnelId: options.tunnelId,
|
|
3794
|
-
localHost: options.host,
|
|
3885
|
+
tunnelId: options.tunnelId || undefined,
|
|
3886
|
+
localHost: options.host || undefined,
|
|
3795
3887
|
baseDomain: 'kimaki.dev',
|
|
3796
|
-
serverUrl: options.server,
|
|
3888
|
+
serverUrl: options.server || undefined,
|
|
3797
3889
|
command: command.length > 0 ? command : undefined,
|
|
3798
3890
|
kill: options.kill,
|
|
3799
3891
|
})
|
package/src/commands/agent.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
import { initializeOpencodeForDirectory } from '../opencode.js'
|
|
24
24
|
import { resolveTextChannel, getKimakiMetadata } from '../discord-utils.js'
|
|
25
25
|
import { createLogger, LogPrefix } from '../logger.js'
|
|
26
|
+
import { getCurrentModelInfo } from './model.js'
|
|
26
27
|
|
|
27
28
|
const agentLogger = createLogger(LogPrefix.AGENT)
|
|
28
29
|
|
|
@@ -455,13 +456,34 @@ export async function handleQuickAgentCommand({
|
|
|
455
456
|
? ` (was **${previousAgentName}**)`
|
|
456
457
|
: ''
|
|
457
458
|
|
|
459
|
+
// Resolve the model that will now be used for the new agent so we can
|
|
460
|
+
// show it in the reply. setAgentForContext already cleared any session
|
|
461
|
+
// model preference, so getCurrentModelInfo falls through to the agent's
|
|
462
|
+
// configured model (or channel/global/default).
|
|
463
|
+
const modelInfo = await (async () => {
|
|
464
|
+
const getClient = await initializeOpencodeForDirectory(context.dir)
|
|
465
|
+
if (getClient instanceof Error) {
|
|
466
|
+
return { type: 'none' as const }
|
|
467
|
+
}
|
|
468
|
+
return getCurrentModelInfo({
|
|
469
|
+
sessionId: context.sessionId,
|
|
470
|
+
channelId: context.channelId,
|
|
471
|
+
appId,
|
|
472
|
+
agentPreference: resolvedAgentName,
|
|
473
|
+
getClient,
|
|
474
|
+
})
|
|
475
|
+
})()
|
|
476
|
+
|
|
477
|
+
const modelText =
|
|
478
|
+
modelInfo.type === 'none' ? '' : `\nModel: *${modelInfo.model}*`
|
|
479
|
+
|
|
458
480
|
if (context.isThread && context.sessionId) {
|
|
459
481
|
await command.editReply({
|
|
460
|
-
content: `Switched to **${resolvedAgentName}** agent for this session${previousText}\nThe agent will change on the next message.`,
|
|
482
|
+
content: `Switched to **${resolvedAgentName}** agent for this session${previousText}${modelText}\nThe agent will change on the next message.`,
|
|
461
483
|
})
|
|
462
484
|
} else {
|
|
463
485
|
await command.editReply({
|
|
464
|
-
content: `Switched to **${resolvedAgentName}** agent for this channel${previousText}\nAll new sessions will use this agent.`,
|
|
486
|
+
content: `Switched to **${resolvedAgentName}** agent for this channel${previousText}${modelText}\nAll new sessions will use this agent.`,
|
|
465
487
|
})
|
|
466
488
|
}
|
|
467
489
|
} catch (error) {
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// Tests AskUserQuestion request deduplication and cleanup helpers.
|
|
2
|
+
|
|
3
|
+
import { afterEach, describe, expect, test, vi } from 'vitest'
|
|
4
|
+
import type { ThreadChannel } from 'discord.js'
|
|
5
|
+
import {
|
|
6
|
+
deletePendingQuestionContextsForRequest,
|
|
7
|
+
pendingQuestionContexts,
|
|
8
|
+
showAskUserQuestionDropdowns,
|
|
9
|
+
} from './ask-question.js'
|
|
10
|
+
|
|
11
|
+
function createFakeThread(): ThreadChannel {
|
|
12
|
+
const send = vi.fn(async () => {
|
|
13
|
+
return { id: 'msg-1' }
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
id: 'thread-1',
|
|
18
|
+
send,
|
|
19
|
+
} as unknown as ThreadChannel
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
pendingQuestionContexts.clear()
|
|
24
|
+
vi.restoreAllMocks()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe('ask-question', () => {
|
|
28
|
+
test('dedupes duplicate question requests for the same thread', async () => {
|
|
29
|
+
const thread = createFakeThread()
|
|
30
|
+
|
|
31
|
+
await showAskUserQuestionDropdowns({
|
|
32
|
+
thread,
|
|
33
|
+
sessionId: 'ses-1',
|
|
34
|
+
directory: '/project',
|
|
35
|
+
requestId: 'req-1',
|
|
36
|
+
input: {
|
|
37
|
+
questions: [{
|
|
38
|
+
question: 'Choose one',
|
|
39
|
+
header: 'Pick',
|
|
40
|
+
options: [
|
|
41
|
+
{ label: 'Alpha', description: 'A' },
|
|
42
|
+
{ label: 'Beta', description: 'B' },
|
|
43
|
+
],
|
|
44
|
+
}],
|
|
45
|
+
},
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
await showAskUserQuestionDropdowns({
|
|
49
|
+
thread,
|
|
50
|
+
sessionId: 'ses-1',
|
|
51
|
+
directory: '/project',
|
|
52
|
+
requestId: 'req-1',
|
|
53
|
+
input: {
|
|
54
|
+
questions: [{
|
|
55
|
+
question: 'Choose one',
|
|
56
|
+
header: 'Pick',
|
|
57
|
+
options: [
|
|
58
|
+
{ label: 'Alpha', description: 'A' },
|
|
59
|
+
{ label: 'Beta', description: 'B' },
|
|
60
|
+
],
|
|
61
|
+
}],
|
|
62
|
+
},
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
expect(thread.send).toHaveBeenCalledTimes(1)
|
|
66
|
+
expect(pendingQuestionContexts.size).toBe(1)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('removes all duplicate contexts for one request', () => {
|
|
70
|
+
const thread = createFakeThread()
|
|
71
|
+
const baseContext: typeof pendingQuestionContexts extends Map<string, infer T>
|
|
72
|
+
? T
|
|
73
|
+
: never = {
|
|
74
|
+
sessionId: 'ses-1',
|
|
75
|
+
directory: '/project',
|
|
76
|
+
thread,
|
|
77
|
+
requestId: 'req-1',
|
|
78
|
+
questions: [{
|
|
79
|
+
question: 'Choose one',
|
|
80
|
+
header: 'Pick',
|
|
81
|
+
options: [
|
|
82
|
+
{ label: 'Alpha', description: 'A' },
|
|
83
|
+
{ label: 'Beta', description: 'B' },
|
|
84
|
+
],
|
|
85
|
+
}],
|
|
86
|
+
answers: {},
|
|
87
|
+
totalQuestions: 1,
|
|
88
|
+
answeredCount: 0,
|
|
89
|
+
contextHash: 'ctx-1',
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
pendingQuestionContexts.set('ctx-1', baseContext)
|
|
93
|
+
pendingQuestionContexts.set('ctx-2', {
|
|
94
|
+
...baseContext,
|
|
95
|
+
contextHash: 'ctx-2',
|
|
96
|
+
})
|
|
97
|
+
pendingQuestionContexts.set('ctx-3', {
|
|
98
|
+
...baseContext,
|
|
99
|
+
requestId: 'req-2',
|
|
100
|
+
contextHash: 'ctx-3',
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
const removed = deletePendingQuestionContextsForRequest({
|
|
104
|
+
threadId: thread.id,
|
|
105
|
+
requestId: 'req-1',
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
expect(removed).toBe(2)
|
|
109
|
+
expect([...pendingQuestionContexts.keys()]).toEqual(['ctx-3'])
|
|
110
|
+
})
|
|
111
|
+
})
|