@tduyng/pi-opencode-z 0.0.0-stage → 0.0.1
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/LICENSE +178 -0
- package/README.md +69 -2
- package/package.json +42 -4
- package/src/client.ts +280 -0
- package/src/config.ts +96 -0
- package/src/index.ts +195 -0
- package/src/mcp.ts +315 -0
- package/src/models.ts +86 -0
- package/src/session.ts +191 -0
- package/src/stream.ts +1013 -0
package/src/session.ts
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto'
|
|
2
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
|
|
6
|
+
import type { Message, Tool } from '@earendil-works/pi-ai'
|
|
7
|
+
|
|
8
|
+
import { api } from './client.js'
|
|
9
|
+
import type { JsonObject, McpResult } from './mcp.js'
|
|
10
|
+
|
|
11
|
+
export const MCP_SERVER_NAME = 'pi'
|
|
12
|
+
export const TOOL_PREFIX = `${MCP_SERVER_NAME}_`
|
|
13
|
+
export const BRIDGE_AGENT = 'pi'
|
|
14
|
+
|
|
15
|
+
const MCP_EXECUTION_TIMEOUT_MS = 300_000
|
|
16
|
+
|
|
17
|
+
export type ParkedCall = {
|
|
18
|
+
name: string
|
|
19
|
+
args: JsonObject
|
|
20
|
+
resolve(result: McpResult): void
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type TokenTotals = { input: number; output: number; cacheRead: number; cacheWrite: number }
|
|
24
|
+
|
|
25
|
+
export type BridgeSession = {
|
|
26
|
+
token: string
|
|
27
|
+
dir?: string
|
|
28
|
+
opencodeSessionId?: string
|
|
29
|
+
retiredOpencodeIds: Set<string>
|
|
30
|
+
modelId?: string
|
|
31
|
+
variant?: string
|
|
32
|
+
systemPrompt?: string
|
|
33
|
+
toolsKey?: string
|
|
34
|
+
syncedCount: number
|
|
35
|
+
fingerprints: string[]
|
|
36
|
+
tools: Tool[]
|
|
37
|
+
parked: Map<string, ParkedCall>
|
|
38
|
+
answeredPermissions: Set<string>
|
|
39
|
+
refusedTools: string[]
|
|
40
|
+
lateUsage: TokenTotals
|
|
41
|
+
latestStep: TokenTotals
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type BridgeSessionStore = { sessions: Map<string, BridgeSession> }
|
|
45
|
+
|
|
46
|
+
export function createSessionStore(): BridgeSessionStore {
|
|
47
|
+
return { sessions: new Map() }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function bridgeSession(store: BridgeSessionStore, piSessionId: string): BridgeSession {
|
|
51
|
+
const existing = store.sessions.get(piSessionId)
|
|
52
|
+
if (existing) return existing
|
|
53
|
+
|
|
54
|
+
const created: BridgeSession = {
|
|
55
|
+
token: randomBytes(12).toString('hex'),
|
|
56
|
+
retiredOpencodeIds: new Set(),
|
|
57
|
+
syncedCount: 0,
|
|
58
|
+
fingerprints: [],
|
|
59
|
+
tools: [],
|
|
60
|
+
parked: new Map(),
|
|
61
|
+
answeredPermissions: new Set(),
|
|
62
|
+
refusedTools: [],
|
|
63
|
+
lateUsage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
64
|
+
latestStep: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }
|
|
65
|
+
}
|
|
66
|
+
store.sessions.set(piSessionId, created)
|
|
67
|
+
return created
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function allSessions(store: BridgeSessionStore): BridgeSession[] {
|
|
71
|
+
return [...store.sessions.values()]
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function findByOpencodeId(store: BridgeSessionStore, opencodeSessionId: string): BridgeSession | undefined {
|
|
75
|
+
for (const session of store.sessions.values()) {
|
|
76
|
+
if (session.opencodeSessionId === opencodeSessionId) return session
|
|
77
|
+
if (session.retiredOpencodeIds.has(opencodeSessionId)) return session
|
|
78
|
+
}
|
|
79
|
+
return undefined
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function messageFingerprint(message: Message): string {
|
|
83
|
+
const value =
|
|
84
|
+
message.role === 'toolResult'
|
|
85
|
+
? [message.role, message.toolCallId, message.toolName, message.isError, message.content]
|
|
86
|
+
: [message.role, message.content]
|
|
87
|
+
return createHash('sha256').update(JSON.stringify(value)).digest('base64url')
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function toolsKey(tools: Tool[] | undefined): string {
|
|
91
|
+
return JSON.stringify(
|
|
92
|
+
(tools ?? []).map((tool) => ({
|
|
93
|
+
name: tool.name,
|
|
94
|
+
description: tool.description,
|
|
95
|
+
parameters: tool.parameters
|
|
96
|
+
}))
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const AGENT_SYSTEM = [
|
|
101
|
+
'You are the model behind the Pi coding agent.',
|
|
102
|
+
'',
|
|
103
|
+
'Pi tools are MCP tools named pi_*. Call the exact declared name, such as pi_read or pi_bash. A functions. namespace label does not rename a tool.',
|
|
104
|
+
'OpenCode built-ins such as read, edit, write, bash, glob and grep are disabled. Use their pi_ counterparts instead.',
|
|
105
|
+
'Your working directory is not the project. Put project paths in pi_ tool arguments.',
|
|
106
|
+
'Answer in plain prose when no tool is needed.'
|
|
107
|
+
].join('\n')
|
|
108
|
+
|
|
109
|
+
function bridgeConfig(mcpUrl: string): string {
|
|
110
|
+
return `${JSON.stringify(
|
|
111
|
+
{
|
|
112
|
+
mcp: {
|
|
113
|
+
timeout: { startup: 15_000, catalog: 15_000, execution: MCP_EXECUTION_TIMEOUT_MS },
|
|
114
|
+
servers: {
|
|
115
|
+
[MCP_SERVER_NAME]: {
|
|
116
|
+
type: 'remote',
|
|
117
|
+
url: mcpUrl,
|
|
118
|
+
codemode: false,
|
|
119
|
+
timeout: { execution: MCP_EXECUTION_TIMEOUT_MS }
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
agents: {
|
|
124
|
+
[BRIDGE_AGENT]: {
|
|
125
|
+
description: 'Pi bridge',
|
|
126
|
+
mode: 'primary',
|
|
127
|
+
system: AGENT_SYSTEM,
|
|
128
|
+
permissions: [
|
|
129
|
+
{ action: '*', resource: '*', effect: 'ask' },
|
|
130
|
+
{ action: `${TOOL_PREFIX}*`, resource: '*', effect: 'allow' }
|
|
131
|
+
]
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
null,
|
|
136
|
+
2
|
|
137
|
+
)}\n`
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function ensureProjectDir(session: BridgeSession, mcpUrl: string): Promise<string> {
|
|
141
|
+
if (session.dir) return session.dir
|
|
142
|
+
|
|
143
|
+
const dir = await mkdtemp(join(tmpdir(), 'pi-opencode-z-'))
|
|
144
|
+
await writeFile(join(dir, 'opencode.jsonc'), bridgeConfig(mcpUrl), 'utf8')
|
|
145
|
+
session.dir = dir
|
|
146
|
+
return dir
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function resetOpencodeSession(session: BridgeSession): Promise<void> {
|
|
150
|
+
const id = session.opencodeSessionId
|
|
151
|
+
session.opencodeSessionId = undefined
|
|
152
|
+
if (id) {
|
|
153
|
+
session.retiredOpencodeIds.add(id)
|
|
154
|
+
await api('DELETE', `/api/session/${id}`).catch(() => undefined)
|
|
155
|
+
}
|
|
156
|
+
session.modelId = undefined
|
|
157
|
+
session.variant = undefined
|
|
158
|
+
session.systemPrompt = undefined
|
|
159
|
+
session.toolsKey = undefined
|
|
160
|
+
session.syncedCount = 0
|
|
161
|
+
session.fingerprints = []
|
|
162
|
+
session.refusedTools = []
|
|
163
|
+
releaseParked(session, 'The bridge restarted this session; the call was dropped.')
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function releaseParked(session: BridgeSession, reason: string): void {
|
|
167
|
+
for (const call of session.parked.values()) {
|
|
168
|
+
call.resolve({ content: [{ type: 'text', text: reason }], isError: true })
|
|
169
|
+
}
|
|
170
|
+
session.parked.clear()
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function disposeSession(
|
|
174
|
+
store: BridgeSessionStore,
|
|
175
|
+
piSessionId: string,
|
|
176
|
+
unregister: (token: string) => void
|
|
177
|
+
): Promise<void> {
|
|
178
|
+
const session = store.sessions.get(piSessionId)
|
|
179
|
+
if (!session) return
|
|
180
|
+
store.sessions.delete(piSessionId)
|
|
181
|
+
unregister(session.token)
|
|
182
|
+
releaseParked(session, 'The Pi session ended.')
|
|
183
|
+
if (session.opencodeSessionId) {
|
|
184
|
+
await api('DELETE', `/api/session/${session.opencodeSessionId}`).catch(() => undefined)
|
|
185
|
+
}
|
|
186
|
+
if (session.dir) await rm(session.dir, { recursive: true, force: true }).catch(() => undefined)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export async function disposeAll(store: BridgeSessionStore, unregister: (token: string) => void): Promise<void> {
|
|
190
|
+
await Promise.all([...store.sessions.keys()].map((id) => disposeSession(store, id, unregister)))
|
|
191
|
+
}
|