@otto-assistant/bridge 0.4.100 → 0.4.102
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 +227 -178
- package/dist/commands/login.js +6 -4
- package/dist/context-awareness-plugin.js +8 -38
- package/dist/kimaki-opencode-plugin.js +3 -1
- package/dist/memory-overview-plugin.js +126 -0
- package/dist/opencode.js +15 -1
- package/package.json +1 -1
- package/src/anthropic-auth-plugin.ts +574 -453
- package/src/commands/login.ts +6 -4
- package/src/context-awareness-plugin.ts +11 -42
- package/src/kimaki-opencode-plugin.ts +3 -1
- package/src/memory-overview-plugin.ts +161 -0
- package/src/opencode.ts +22 -1
package/src/commands/login.ts
CHANGED
|
@@ -129,6 +129,8 @@ const PROVIDER_POPULARITY_ORDER: string[] = [
|
|
|
129
129
|
'xai',
|
|
130
130
|
'groq',
|
|
131
131
|
'deepseek',
|
|
132
|
+
'opencode',
|
|
133
|
+
'opencode-go',
|
|
132
134
|
'mistral',
|
|
133
135
|
'openrouter',
|
|
134
136
|
'fireworks-ai',
|
|
@@ -137,12 +139,12 @@ const PROVIDER_POPULARITY_ORDER: string[] = [
|
|
|
137
139
|
'azure',
|
|
138
140
|
'google-vertex',
|
|
139
141
|
'google-vertex-anthropic',
|
|
140
|
-
'cohere',
|
|
142
|
+
// 'cohere',
|
|
141
143
|
'cerebras',
|
|
142
|
-
'perplexity',
|
|
144
|
+
// 'perplexity',
|
|
143
145
|
'cloudflare-workers-ai',
|
|
144
|
-
'novita-ai',
|
|
145
|
-
'huggingface',
|
|
146
|
+
// 'novita-ai',
|
|
147
|
+
// 'huggingface',
|
|
146
148
|
'deepinfra',
|
|
147
149
|
'github-models',
|
|
148
150
|
'lmstudio',
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// OpenCode plugin that injects synthetic message parts for context awareness:
|
|
2
2
|
// - Git branch / detached HEAD changes
|
|
3
3
|
// - Working directory (pwd) changes (e.g. after /new-worktree mid-session)
|
|
4
|
-
// - MEMORY.md table of contents on first message
|
|
5
4
|
// - MEMORY.md reminder after a large assistant reply
|
|
6
5
|
// - Onboarding tutorial instructions (when TUTORIAL_WELCOME_TEXT detected)
|
|
7
6
|
//
|
|
@@ -18,8 +17,6 @@
|
|
|
18
17
|
|
|
19
18
|
import type { Plugin } from '@opencode-ai/plugin'
|
|
20
19
|
import crypto from 'node:crypto'
|
|
21
|
-
import fs from 'node:fs'
|
|
22
|
-
import path from 'node:path'
|
|
23
20
|
import * as errore from 'errore'
|
|
24
21
|
import {
|
|
25
22
|
createPluginLogger,
|
|
@@ -29,7 +26,6 @@ import {
|
|
|
29
26
|
import { setDataDir } from './config.js'
|
|
30
27
|
import { initSentry, notifyError } from './sentry.js'
|
|
31
28
|
import { execAsync } from './exec-async.js'
|
|
32
|
-
import { condenseMemoryMd } from './condense-memory.js'
|
|
33
29
|
import {
|
|
34
30
|
ONBOARDING_TUTORIAL_INSTRUCTIONS,
|
|
35
31
|
TUTORIAL_WELCOME_TEXT,
|
|
@@ -49,7 +45,6 @@ type GitState = {
|
|
|
49
45
|
// All per-session mutable state in one place. One Map entry, one delete.
|
|
50
46
|
type SessionState = {
|
|
51
47
|
gitState: GitState | undefined
|
|
52
|
-
memoryInjected: boolean
|
|
53
48
|
lastMemoryReminderAssistantMessageId: string | undefined
|
|
54
49
|
tutorialInjected: boolean
|
|
55
50
|
// Last directory observed via session.get(). Refreshed on each real user
|
|
@@ -60,17 +55,6 @@ type SessionState = {
|
|
|
60
55
|
announcedDirectory: string | undefined
|
|
61
56
|
}
|
|
62
57
|
|
|
63
|
-
function createSessionState(): SessionState {
|
|
64
|
-
return {
|
|
65
|
-
gitState: undefined,
|
|
66
|
-
memoryInjected: false,
|
|
67
|
-
lastMemoryReminderAssistantMessageId: undefined,
|
|
68
|
-
tutorialInjected: false,
|
|
69
|
-
resolvedDirectory: undefined,
|
|
70
|
-
announcedDirectory: undefined,
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
58
|
// Minimal type for the opencode plugin client (v1 SDK style with path objects).
|
|
75
59
|
type PluginClient = {
|
|
76
60
|
session: {
|
|
@@ -147,10 +131,6 @@ type AssistantMessageInfo = {
|
|
|
147
131
|
tokens?: AssistantTokenUsage
|
|
148
132
|
}
|
|
149
133
|
|
|
150
|
-
function getOutputTokenTotal(tokens: AssistantTokenUsage): number {
|
|
151
|
-
return Math.max(0, tokens.output + tokens.reasoning)
|
|
152
|
-
}
|
|
153
|
-
|
|
154
134
|
export function shouldInjectMemoryReminderFromLatestAssistant({
|
|
155
135
|
lastMemoryReminderAssistantMessageId,
|
|
156
136
|
latestAssistantMessage,
|
|
@@ -175,7 +155,10 @@ export function shouldInjectMemoryReminderFromLatestAssistant({
|
|
|
175
155
|
if (lastMemoryReminderAssistantMessageId === latestAssistantMessage.id) {
|
|
176
156
|
return { inject: false }
|
|
177
157
|
}
|
|
178
|
-
const outputTokens =
|
|
158
|
+
const outputTokens = Math.max(
|
|
159
|
+
0,
|
|
160
|
+
latestAssistantMessage.tokens.output + latestAssistantMessage.tokens.reasoning,
|
|
161
|
+
)
|
|
179
162
|
if (outputTokens < threshold) {
|
|
180
163
|
return { inject: false }
|
|
181
164
|
}
|
|
@@ -311,7 +294,13 @@ const contextAwarenessPlugin: Plugin = async ({ directory, client }) => {
|
|
|
311
294
|
if (existing) {
|
|
312
295
|
return existing
|
|
313
296
|
}
|
|
314
|
-
const state =
|
|
297
|
+
const state: SessionState = {
|
|
298
|
+
gitState: undefined,
|
|
299
|
+
lastMemoryReminderAssistantMessageId: undefined,
|
|
300
|
+
tutorialInjected: false,
|
|
301
|
+
resolvedDirectory: undefined,
|
|
302
|
+
announcedDirectory: undefined,
|
|
303
|
+
}
|
|
315
304
|
sessions.set(sessionID, state)
|
|
316
305
|
return state
|
|
317
306
|
}
|
|
@@ -412,26 +401,6 @@ const contextAwarenessPlugin: Plugin = async ({ directory, client }) => {
|
|
|
412
401
|
})
|
|
413
402
|
}
|
|
414
403
|
|
|
415
|
-
// -- MEMORY.md injection --
|
|
416
|
-
if (!state.memoryInjected) {
|
|
417
|
-
state.memoryInjected = true
|
|
418
|
-
const memoryPath = path.join(effectiveDirectory, 'MEMORY.md')
|
|
419
|
-
const memoryContent = await fs.promises
|
|
420
|
-
.readFile(memoryPath, 'utf-8')
|
|
421
|
-
.catch(() => null)
|
|
422
|
-
if (memoryContent) {
|
|
423
|
-
const condensed = condenseMemoryMd(memoryContent)
|
|
424
|
-
output.parts.push({
|
|
425
|
-
id: `prt_${crypto.randomUUID()}`,
|
|
426
|
-
sessionID,
|
|
427
|
-
messageID,
|
|
428
|
-
type: 'text' as const,
|
|
429
|
-
text: `<system-reminder>Project memory from MEMORY.md (condensed table of contents, line numbers shown):\n${condensed}\nOnly headings are shown above — section bodies are hidden. Use Grep to search MEMORY.md for specific topics, or Read with offset and limit to read a section's content. When writing to MEMORY.md, keep titles concise (under 10 words) and content brief (2-3 sentences max). Only track non-obvious learnings that prevent future mistakes and are not already documented in code comments or AGENTS.md. Do not duplicate information that is self-evident from the code.</system-reminder>`,
|
|
430
|
-
synthetic: true,
|
|
431
|
-
})
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
|
|
435
404
|
const memoryReminder = shouldInjectMemoryReminderFromLatestAssistant({
|
|
436
405
|
lastMemoryReminderAssistantMessageId:
|
|
437
406
|
state.lastMemoryReminderAssistantMessageId,
|
|
@@ -5,12 +5,14 @@
|
|
|
5
5
|
//
|
|
6
6
|
// Plugins are split into focused modules:
|
|
7
7
|
// - ipc-tools-plugin: file upload + action buttons (IPC-based Discord tools)
|
|
8
|
-
// - context-awareness-plugin: branch, pwd, memory
|
|
8
|
+
// - context-awareness-plugin: branch, pwd, memory reminder, onboarding tutorial
|
|
9
|
+
// - memory-overview-plugin: frozen MEMORY.md heading overview per session
|
|
9
10
|
// - opencode-interrupt-plugin: interrupt queued messages at step boundaries
|
|
10
11
|
// - kitty-graphics-plugin: extract Kitty Graphics Protocol images from bash output
|
|
11
12
|
|
|
12
13
|
export { ipcToolsPlugin } from './ipc-tools-plugin.js'
|
|
13
14
|
export { contextAwarenessPlugin } from './context-awareness-plugin.js'
|
|
15
|
+
export { memoryOverviewPlugin } from './memory-overview-plugin.js'
|
|
14
16
|
export { interruptOpencodeSessionOnUserMessage } from './opencode-interrupt-plugin.js'
|
|
15
17
|
export { systemPromptDriftPlugin } from './system-prompt-drift-plugin.js'
|
|
16
18
|
export { anthropicAuthPlugin } from './anthropic-auth-plugin.js'
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// OpenCode plugin that snapshots the MEMORY.md heading overview once per
|
|
2
|
+
// session and injects that frozen snapshot on the first real user message.
|
|
3
|
+
// The snapshot is cached by session ID so later MEMORY.md edits do not change
|
|
4
|
+
// the prompt for the same session and do not invalidate OpenCode's cache.
|
|
5
|
+
|
|
6
|
+
import crypto from 'node:crypto'
|
|
7
|
+
import fs from 'node:fs'
|
|
8
|
+
import path from 'node:path'
|
|
9
|
+
import type { Plugin } from '@opencode-ai/plugin'
|
|
10
|
+
import * as errore from 'errore'
|
|
11
|
+
import {
|
|
12
|
+
createPluginLogger,
|
|
13
|
+
formatPluginErrorWithStack,
|
|
14
|
+
setPluginLogFilePath,
|
|
15
|
+
} from './plugin-logger.js'
|
|
16
|
+
import { condenseMemoryMd } from './condense-memory.js'
|
|
17
|
+
import { initSentry, notifyError } from './sentry.js'
|
|
18
|
+
|
|
19
|
+
const logger = createPluginLogger('OPENCODE')
|
|
20
|
+
|
|
21
|
+
type SessionState = {
|
|
22
|
+
hasFrozenOverview: boolean
|
|
23
|
+
frozenOverviewText: string | null
|
|
24
|
+
injected: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function createSessionState(): SessionState {
|
|
28
|
+
return {
|
|
29
|
+
hasFrozenOverview: false,
|
|
30
|
+
frozenOverviewText: null,
|
|
31
|
+
injected: false,
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function buildMemoryOverviewReminder({ condensed }: { condensed: string }): string {
|
|
36
|
+
return `<system-reminder>Project memory from MEMORY.md (condensed table of contents, line numbers shown):\n${condensed}\nOnly headings are shown above — section bodies are hidden. Use Grep to search MEMORY.md for specific topics, or Read with offset and limit to read a section's content. When writing to MEMORY.md, keep titles concise (under 10 words) and content brief (2-3 sentences max). Only track non-obvious learnings that prevent future mistakes and are not already documented in code comments or AGENTS.md. Do not duplicate information that is self-evident from the code.</system-reminder>`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function freezeMemoryOverview({
|
|
40
|
+
directory,
|
|
41
|
+
state,
|
|
42
|
+
}: {
|
|
43
|
+
directory: string
|
|
44
|
+
state: SessionState
|
|
45
|
+
}): Promise<string | null> {
|
|
46
|
+
if (state.hasFrozenOverview) {
|
|
47
|
+
return state.frozenOverviewText
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const memoryPath = path.join(directory, 'MEMORY.md')
|
|
51
|
+
const memoryContentResult = await fs.promises.readFile(memoryPath, 'utf-8').catch(() => {
|
|
52
|
+
return null
|
|
53
|
+
})
|
|
54
|
+
if (!memoryContentResult) {
|
|
55
|
+
state.hasFrozenOverview = true
|
|
56
|
+
state.frozenOverviewText = null
|
|
57
|
+
return null
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const condensed = condenseMemoryMd(memoryContentResult)
|
|
61
|
+
state.hasFrozenOverview = true
|
|
62
|
+
state.frozenOverviewText = buildMemoryOverviewReminder({ condensed })
|
|
63
|
+
return state.frozenOverviewText
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const memoryOverviewPlugin: Plugin = async ({ directory }) => {
|
|
67
|
+
initSentry()
|
|
68
|
+
|
|
69
|
+
const dataDir = process.env.KIMAKI_DATA_DIR
|
|
70
|
+
if (dataDir) {
|
|
71
|
+
setPluginLogFilePath(dataDir)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const sessions = new Map<string, SessionState>()
|
|
75
|
+
|
|
76
|
+
function getOrCreateSessionState({ sessionID }: { sessionID: string }): SessionState {
|
|
77
|
+
const existing = sessions.get(sessionID)
|
|
78
|
+
if (existing) {
|
|
79
|
+
return existing
|
|
80
|
+
}
|
|
81
|
+
const state = createSessionState()
|
|
82
|
+
sessions.set(sessionID, state)
|
|
83
|
+
return state
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
'chat.message': async (input, output) => {
|
|
88
|
+
const result = await errore.tryAsync({
|
|
89
|
+
try: async () => {
|
|
90
|
+
const state = getOrCreateSessionState({ sessionID: input.sessionID })
|
|
91
|
+
if (state.injected) {
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const firstPart = output.parts.find((part) => {
|
|
96
|
+
if (part.type !== 'text') {
|
|
97
|
+
return true
|
|
98
|
+
}
|
|
99
|
+
return part.synthetic !== true
|
|
100
|
+
})
|
|
101
|
+
if (!firstPart || firstPart.type !== 'text' || firstPart.text.trim().length === 0) {
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const overviewText = await freezeMemoryOverview({ directory, state })
|
|
106
|
+
state.injected = true
|
|
107
|
+
if (!overviewText) {
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
output.parts.push({
|
|
112
|
+
id: `prt_${crypto.randomUUID()}`,
|
|
113
|
+
sessionID: input.sessionID,
|
|
114
|
+
messageID: firstPart.messageID,
|
|
115
|
+
type: 'text' as const,
|
|
116
|
+
text: overviewText,
|
|
117
|
+
synthetic: true,
|
|
118
|
+
})
|
|
119
|
+
},
|
|
120
|
+
catch: (error) => {
|
|
121
|
+
return new Error('memory overview chat.message hook failed', {
|
|
122
|
+
cause: error,
|
|
123
|
+
})
|
|
124
|
+
},
|
|
125
|
+
})
|
|
126
|
+
if (!(result instanceof Error)) {
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
logger.warn(
|
|
130
|
+
`[memory-overview-plugin] ${formatPluginErrorWithStack(result)}`,
|
|
131
|
+
)
|
|
132
|
+
void notifyError(result, 'memory overview plugin chat.message hook failed')
|
|
133
|
+
},
|
|
134
|
+
event: async ({ event }) => {
|
|
135
|
+
const result = await errore.tryAsync({
|
|
136
|
+
try: async () => {
|
|
137
|
+
if (event.type !== 'session.deleted') {
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
const id = event.properties?.info?.id
|
|
141
|
+
if (!id) {
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
sessions.delete(id)
|
|
145
|
+
},
|
|
146
|
+
catch: (error) => {
|
|
147
|
+
return new Error('memory overview event hook failed', {
|
|
148
|
+
cause: error,
|
|
149
|
+
})
|
|
150
|
+
},
|
|
151
|
+
})
|
|
152
|
+
if (!(result instanceof Error)) {
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
logger.warn(`[memory-overview-plugin] ${formatPluginErrorWithStack(result)}`)
|
|
156
|
+
void notifyError(result, 'memory overview plugin event hook failed')
|
|
157
|
+
},
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export { memoryOverviewPlugin }
|
package/src/opencode.ts
CHANGED
|
@@ -489,6 +489,9 @@ async function startSingleServer(): Promise<ServerStartError | SingleServer> {
|
|
|
489
489
|
const opencodeConfigDir = path
|
|
490
490
|
.join(os.homedir(), '.config', 'opencode')
|
|
491
491
|
.replaceAll('\\', '/')
|
|
492
|
+
const opensrcDir = path
|
|
493
|
+
.join(os.homedir(), '.opensrc')
|
|
494
|
+
.replaceAll('\\', '/')
|
|
492
495
|
const kimakiDataDir = path
|
|
493
496
|
.join(os.homedir(), '.kimaki')
|
|
494
497
|
.replaceAll('\\', '/')
|
|
@@ -503,6 +506,8 @@ async function startSingleServer(): Promise<ServerStartError | SingleServer> {
|
|
|
503
506
|
[`${tmpdir}/*`]: 'allow',
|
|
504
507
|
[opencodeConfigDir]: 'allow',
|
|
505
508
|
[`${opencodeConfigDir}/*`]: 'allow',
|
|
509
|
+
[opensrcDir]: 'allow',
|
|
510
|
+
[`${opensrcDir}/*`]: 'allow',
|
|
506
511
|
[kimakiDataDir]: 'allow',
|
|
507
512
|
[`${kimakiDataDir}/*`]: 'allow',
|
|
508
513
|
}
|
|
@@ -543,11 +548,17 @@ async function startSingleServer(): Promise<ServerStartError | SingleServer> {
|
|
|
543
548
|
// priority chain, so project-level opencode.json can override kimaki defaults.
|
|
544
549
|
// OPENCODE_CONFIG_CONTENT was loaded last and overrode user project configs,
|
|
545
550
|
// causing issue #90 (project permissions not being respected).
|
|
551
|
+
const isDev = import.meta.url.endsWith('.ts') || import.meta.url.endsWith('.tsx')
|
|
546
552
|
const opencodeConfig = {
|
|
547
553
|
$schema: 'https://opencode.ai/config.json',
|
|
548
554
|
lsp: false,
|
|
549
555
|
formatter: false,
|
|
550
|
-
plugin: [
|
|
556
|
+
plugin: [
|
|
557
|
+
new URL(
|
|
558
|
+
isDev ? './kimaki-opencode-plugin.ts' : './kimaki-opencode-plugin.js',
|
|
559
|
+
import.meta.url,
|
|
560
|
+
).href,
|
|
561
|
+
],
|
|
551
562
|
permission: {
|
|
552
563
|
edit: 'allow',
|
|
553
564
|
bash: 'allow',
|
|
@@ -878,6 +889,16 @@ export function buildSessionPermissions({
|
|
|
878
889
|
{ permission: 'external_directory', pattern: `${opencodeConfigDir}/*`, action: 'allow' },
|
|
879
890
|
)
|
|
880
891
|
|
|
892
|
+
// Allow ~/.opensrc so agents can inspect cached opensrc checkouts without
|
|
893
|
+
// permission prompts.
|
|
894
|
+
const opensrcDir = path
|
|
895
|
+
.join(os.homedir(), '.opensrc')
|
|
896
|
+
.replaceAll('\\', '/')
|
|
897
|
+
rules.push(
|
|
898
|
+
{ permission: 'external_directory', pattern: opensrcDir, action: 'allow' },
|
|
899
|
+
{ permission: 'external_directory', pattern: `${opensrcDir}/*`, action: 'allow' },
|
|
900
|
+
)
|
|
901
|
+
|
|
881
902
|
// Allow ~/.kimaki so the agent can access kimaki data dir (logs, db, etc.)
|
|
882
903
|
// without permission prompts.
|
|
883
904
|
const kimakiDataDir = path
|