@tduyng/pi-claude 0.0.0-stage → 0.0.2
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 +86 -2
- package/package.json +40 -4
- package/src/index.ts +170 -0
- package/src/json.ts +74 -0
- package/src/mcp.ts +288 -0
- package/src/protocol.ts +335 -0
- package/src/session.ts +237 -0
- package/src/transcript.ts +213 -0
- package/src/turn.ts +657 -0
package/src/session.ts
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
import type { AssistantMessage, Tool, ToolResultMessage } from '@earendil-works/pi-ai'
|
|
4
|
+
|
|
5
|
+
import { jsonText, sha, stableStringify, type JsonObject } from './json.js'
|
|
6
|
+
import type { McpResult } from './mcp.js'
|
|
7
|
+
import { ClaudeProcess, type ClaudeSpec } from './protocol.js'
|
|
8
|
+
import { fingerprint, toolNameFromMcp, toolResultMcp, type Transcript } from './transcript.js'
|
|
9
|
+
|
|
10
|
+
const MAX_SESSIONS = 8
|
|
11
|
+
|
|
12
|
+
export type SessionKey = {
|
|
13
|
+
model: string
|
|
14
|
+
systemPrompt: string
|
|
15
|
+
toolsHash: string
|
|
16
|
+
effort: string
|
|
17
|
+
cwd: string
|
|
18
|
+
contextWindow: number
|
|
19
|
+
maxTokens: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type ClaudeSession = {
|
|
23
|
+
token: string
|
|
24
|
+
key: SessionKey
|
|
25
|
+
spec: ClaudeSpec
|
|
26
|
+
tools: Tool[]
|
|
27
|
+
proc?: ClaudeProcess
|
|
28
|
+
chain: string
|
|
29
|
+
count: number
|
|
30
|
+
awaiting: Set<string>
|
|
31
|
+
parked: Map<string, (result: McpResult) => void>
|
|
32
|
+
ready: Map<string, McpResult>
|
|
33
|
+
servedByClaude: Set<string>
|
|
34
|
+
pendingTools: { id: string; name: string; args: JsonObject }[]
|
|
35
|
+
busy: boolean
|
|
36
|
+
dead: boolean
|
|
37
|
+
lastUsed: number
|
|
38
|
+
settling?: Promise<void>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type ClaudeSessionStore = { sessions: Map<string, ClaudeSession> }
|
|
42
|
+
|
|
43
|
+
export type Attached = { session: ClaudeSession; prefix: number }
|
|
44
|
+
|
|
45
|
+
export function createSessionStore(): ClaudeSessionStore {
|
|
46
|
+
return { sessions: new Map() }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function allSessions(store: ClaudeSessionStore): ClaudeSession[] {
|
|
50
|
+
return [...store.sessions.values()]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function newSessionToken(): string {
|
|
54
|
+
return randomBytes(12).toString('hex')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function sessionFor(
|
|
58
|
+
store: ClaudeSessionStore,
|
|
59
|
+
id: string,
|
|
60
|
+
token: string,
|
|
61
|
+
key: SessionKey,
|
|
62
|
+
spec: ClaudeSpec,
|
|
63
|
+
tools: Tool[],
|
|
64
|
+
transcript: Transcript
|
|
65
|
+
): ClaudeSession {
|
|
66
|
+
const session: ClaudeSession = {
|
|
67
|
+
token,
|
|
68
|
+
key,
|
|
69
|
+
spec,
|
|
70
|
+
tools,
|
|
71
|
+
chain: transcript.chain[transcript.messages.length] ?? transcript.chain[0] ?? sha('prompt'),
|
|
72
|
+
count: transcript.messages.length,
|
|
73
|
+
awaiting: new Set(),
|
|
74
|
+
parked: new Map(),
|
|
75
|
+
ready: new Map(),
|
|
76
|
+
servedByClaude: new Set(),
|
|
77
|
+
pendingTools: [],
|
|
78
|
+
busy: true,
|
|
79
|
+
dead: false,
|
|
80
|
+
lastUsed: Date.now()
|
|
81
|
+
}
|
|
82
|
+
store.sessions.set(id, session)
|
|
83
|
+
evict(store)
|
|
84
|
+
return session
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function attachSession(
|
|
88
|
+
store: ClaudeSessionStore,
|
|
89
|
+
transcript: Transcript,
|
|
90
|
+
key: SessionKey
|
|
91
|
+
): Attached | undefined {
|
|
92
|
+
for (const session of store.sessions.values()) {
|
|
93
|
+
if (session.dead || session.busy) continue
|
|
94
|
+
if (!sameKey(session.key, key)) continue
|
|
95
|
+
if (session.count > transcript.messages.length) continue
|
|
96
|
+
if (transcript.chain[session.count] !== session.chain) continue
|
|
97
|
+
|
|
98
|
+
const tail = transcript.messages.slice(session.count)
|
|
99
|
+
if (tail.some((message) => message.role === 'assistant')) continue
|
|
100
|
+
|
|
101
|
+
const resultIds = tail.flatMap((message) => (message.role === 'toolResult' ? [message.toolCallId] : []))
|
|
102
|
+
if (!resultIds.every((id) => session.awaiting.has(id))) continue
|
|
103
|
+
|
|
104
|
+
session.busy = true
|
|
105
|
+
session.lastUsed = Date.now()
|
|
106
|
+
return { session, prefix: session.count }
|
|
107
|
+
}
|
|
108
|
+
return undefined
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function sameKey(left: SessionKey, right: SessionKey): boolean {
|
|
112
|
+
return (
|
|
113
|
+
left.model === right.model &&
|
|
114
|
+
left.systemPrompt === right.systemPrompt &&
|
|
115
|
+
left.toolsHash === right.toolsHash &&
|
|
116
|
+
left.effort === right.effort &&
|
|
117
|
+
left.cwd === right.cwd &&
|
|
118
|
+
left.contextWindow === right.contextWindow &&
|
|
119
|
+
left.maxTokens === right.maxTokens
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function updateChain(session: ClaudeSession, transcript: Transcript, output: AssistantMessage): void {
|
|
124
|
+
const base = transcript.chain[transcript.messages.length] ?? session.chain
|
|
125
|
+
session.chain = sha(`${base}\0${fingerprint(output)}`)
|
|
126
|
+
session.count = transcript.messages.length + 1
|
|
127
|
+
session.lastUsed = Date.now()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function parkToolCall(
|
|
131
|
+
session: ClaudeSession,
|
|
132
|
+
name: string,
|
|
133
|
+
args: JsonObject,
|
|
134
|
+
meta: JsonObject
|
|
135
|
+
): Promise<McpResult> {
|
|
136
|
+
const toolName = toolNameFromMcp(name)
|
|
137
|
+
if (!session.tools.some((tool) => tool.name === toolName)) {
|
|
138
|
+
return Promise.resolve({
|
|
139
|
+
content: [{ type: 'text', text: `Pi bridge: unknown tool ${toolName}` }],
|
|
140
|
+
isError: true
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
let id = jsonText(meta, 'claudecode/toolUseId')
|
|
144
|
+
if (id) {
|
|
145
|
+
const index = session.pendingTools.findIndex((call) => call.id === id)
|
|
146
|
+
if (index >= 0) session.pendingTools.splice(index, 1)
|
|
147
|
+
} else {
|
|
148
|
+
const index = session.pendingTools.findIndex(
|
|
149
|
+
(call) => call.name === toolName && stableStringify(call.args) === stableStringify(args)
|
|
150
|
+
)
|
|
151
|
+
const call = index >= 0 ? session.pendingTools.splice(index, 1)[0] : undefined
|
|
152
|
+
id = call?.id
|
|
153
|
+
}
|
|
154
|
+
if (!id) {
|
|
155
|
+
return Promise.resolve({
|
|
156
|
+
content: [{ type: 'text', text: 'Pi bridge: tool call without a tool_use id' }],
|
|
157
|
+
isError: true
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
const ready = session.ready.get(id)
|
|
161
|
+
if (ready) {
|
|
162
|
+
session.ready.delete(id)
|
|
163
|
+
return Promise.resolve(ready)
|
|
164
|
+
}
|
|
165
|
+
if (session.parked.has(id)) {
|
|
166
|
+
return Promise.resolve({ content: [{ type: 'text', text: 'Pi bridge: duplicate tool call' }], isError: true })
|
|
167
|
+
}
|
|
168
|
+
return new Promise((resolve) => {
|
|
169
|
+
session.parked.set(id, resolve)
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function deliverToolResult(session: ClaudeSession, message: ToolResultMessage): void {
|
|
174
|
+
const result = toolResultMcp(message)
|
|
175
|
+
const resolve = session.parked.get(message.toolCallId)
|
|
176
|
+
if (resolve) {
|
|
177
|
+
session.parked.delete(message.toolCallId)
|
|
178
|
+
resolve(result)
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
session.ready.set(message.toolCallId, result)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function failMissingResults(session: ClaudeSession, seen: Set<string>): void {
|
|
185
|
+
for (const id of session.awaiting) {
|
|
186
|
+
if (seen.has(id)) continue
|
|
187
|
+
const resolve = session.parked.get(id)
|
|
188
|
+
const result: McpResult = {
|
|
189
|
+
content: [{ type: 'text', text: 'Pi did not return a result for this tool call.' }],
|
|
190
|
+
isError: true
|
|
191
|
+
}
|
|
192
|
+
if (resolve) {
|
|
193
|
+
session.parked.delete(id)
|
|
194
|
+
resolve(result)
|
|
195
|
+
} else {
|
|
196
|
+
session.ready.set(id, result)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function failParked(session: ClaudeSession, reason: string): void {
|
|
202
|
+
for (const resolve of session.parked.values()) {
|
|
203
|
+
resolve({ content: [{ type: 'text', text: reason }], isError: true })
|
|
204
|
+
}
|
|
205
|
+
session.parked.clear()
|
|
206
|
+
session.ready.clear()
|
|
207
|
+
session.pendingTools.length = 0
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function dropSession(store: ClaudeSessionStore, session: ClaudeSession): void {
|
|
211
|
+
session.dead = true
|
|
212
|
+
for (const [id, candidate] of store.sessions) if (candidate === session) store.sessions.delete(id)
|
|
213
|
+
failParked(session, 'The Pi session closed this Claude Code process.')
|
|
214
|
+
void session.proc?.close()
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function dropSessions(store: ClaudeSessionStore): void {
|
|
218
|
+
for (const session of store.sessions.values()) dropSession(store, session)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function sweep(store: ClaudeSessionStore, idleMs: number): void {
|
|
222
|
+
const now = Date.now()
|
|
223
|
+
for (const session of store.sessions.values()) {
|
|
224
|
+
if (!session.busy && now - session.lastUsed > idleMs) dropSession(store, session)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function evict(store: ClaudeSessionStore): void {
|
|
229
|
+
if (store.sessions.size <= MAX_SESSIONS) return
|
|
230
|
+
const idle = [...store.sessions.values()]
|
|
231
|
+
.filter((session) => !session.busy && session.awaiting.size === 0 && session.parked.size === 0)
|
|
232
|
+
.toSorted((left, right) => left.lastUsed - right.lastUsed)
|
|
233
|
+
while (store.sessions.size > MAX_SESSIONS && idle.length > 0) {
|
|
234
|
+
const session = idle.shift()
|
|
235
|
+
if (session) dropSession(store, session)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getCurrentTools,
|
|
3
|
+
getInitialSystemMessage,
|
|
4
|
+
getSystemMessageText,
|
|
5
|
+
renderSystemMessageUpdate,
|
|
6
|
+
toToolDeclaration,
|
|
7
|
+
type AssistantMessage,
|
|
8
|
+
type ImageContent,
|
|
9
|
+
type Message,
|
|
10
|
+
type SystemMessage,
|
|
11
|
+
type TextContent,
|
|
12
|
+
type Tool,
|
|
13
|
+
type ToolResultMessage,
|
|
14
|
+
type UserMessage
|
|
15
|
+
} from '@earendil-works/pi-ai'
|
|
16
|
+
|
|
17
|
+
import { jsonObject, sha, stableStringify, type JsonValue } from './json.js'
|
|
18
|
+
import { mcpInputSchema, type McpResult, type McpToolDef } from './mcp.js'
|
|
19
|
+
|
|
20
|
+
export type ClaudeContext = { messages: Message[] }
|
|
21
|
+
export type TurnMessage = UserMessage | AssistantMessage | ToolResultMessage | SystemMessage
|
|
22
|
+
|
|
23
|
+
export type Transcript = {
|
|
24
|
+
systemPrompt: string
|
|
25
|
+
tools: Tool[]
|
|
26
|
+
toolsHash: string
|
|
27
|
+
messages: TurnMessage[]
|
|
28
|
+
chain: string[]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type CliTextBlock = { type: 'text'; text: string }
|
|
32
|
+
export type CliImageBlock = { type: 'image'; source: { type: 'base64'; media_type: string; data: string } }
|
|
33
|
+
export type CliBlock = CliTextBlock | CliImageBlock
|
|
34
|
+
|
|
35
|
+
const MCP_PREFIX = 'mcp__pi__'
|
|
36
|
+
|
|
37
|
+
export function readTranscript(context: ClaudeContext): Transcript {
|
|
38
|
+
const leading = getInitialSystemMessage(context.messages)
|
|
39
|
+
const systemPrompt = leading ? getSystemMessageText(leading) : ''
|
|
40
|
+
const tools = getCurrentTools(context.messages).map(toToolDeclaration)
|
|
41
|
+
const messages: TurnMessage[] = []
|
|
42
|
+
const dropped = new Set<string>()
|
|
43
|
+
|
|
44
|
+
for (const message of context.messages) {
|
|
45
|
+
if (message === leading) continue
|
|
46
|
+
if (message.role === 'assistant' && (message.stopReason === 'error' || message.stopReason === 'aborted')) {
|
|
47
|
+
for (const block of message.content) if (block.type === 'toolCall') dropped.add(block.id)
|
|
48
|
+
continue
|
|
49
|
+
}
|
|
50
|
+
if (message.role === 'toolResult' && dropped.has(message.toolCallId)) continue
|
|
51
|
+
messages.push(message)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const chain = [sha(`prompt\0${systemPrompt}`)]
|
|
55
|
+
for (const message of messages) chain.push(sha(`${chain[chain.length - 1]}\0${fingerprint(message)}`))
|
|
56
|
+
return { systemPrompt, tools, toolsHash: toolsHash(tools), messages, chain }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function fingerprint(message: TurnMessage): string {
|
|
60
|
+
switch (message.role) {
|
|
61
|
+
case 'user':
|
|
62
|
+
return stableStringify(['u', normalContent(message.content)])
|
|
63
|
+
case 'toolResult':
|
|
64
|
+
return stableStringify(['r', message.toolCallId, normalContent(message.content), message.isError])
|
|
65
|
+
case 'system':
|
|
66
|
+
return stableStringify(['s', renderSystemMessageUpdate(message)])
|
|
67
|
+
case 'assistant': {
|
|
68
|
+
const blocks: JsonValue[] = []
|
|
69
|
+
for (const block of message.content) {
|
|
70
|
+
if (block.type === 'text') {
|
|
71
|
+
if (block.text.length > 0) blocks.push(['t', block.text])
|
|
72
|
+
} else if (block.type === 'thinking') {
|
|
73
|
+
blocks.push(['k', block.thinking, block.thinkingSignature ?? ''])
|
|
74
|
+
} else {
|
|
75
|
+
const args: JsonValue = JSON.parse(JSON.stringify(block.arguments))
|
|
76
|
+
blocks.push(['c', block.id, block.name, args])
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return stableStringify(['a', blocks])
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function normalContent(content: string | (TextContent | ImageContent)[]): JsonValue {
|
|
85
|
+
if (!Array.isArray(content)) return [['t', content]]
|
|
86
|
+
return content.map((block) => (block.type === 'text' ? ['t', block.text] : ['i', block.mimeType, sha(block.data)]))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function toolsHash(tools: Tool[]): string {
|
|
90
|
+
return sha(
|
|
91
|
+
JSON.stringify(
|
|
92
|
+
tools.map((tool) => ({ name: tool.name, description: tool.description, parameters: tool.parameters }))
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function toolNameFromMcp(name: string): string {
|
|
98
|
+
return name.startsWith(MCP_PREFIX) ? name.slice(MCP_PREFIX.length) : name
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function mcpToolDefs(tools: Tool[]): McpToolDef[] {
|
|
102
|
+
return tools.map((tool) => {
|
|
103
|
+
const parameters = jsonObject(JSON.parse(JSON.stringify(tool.parameters)))
|
|
104
|
+
return {
|
|
105
|
+
name: tool.name,
|
|
106
|
+
description: tool.description,
|
|
107
|
+
inputSchema: mcpInputSchema(parameters)
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function toolResultMcp(message: ToolResultMessage): McpResult {
|
|
113
|
+
const content = message.content.map((block) =>
|
|
114
|
+
block.type === 'text'
|
|
115
|
+
? { type: 'text' as const, text: block.text }
|
|
116
|
+
: { type: 'image' as const, data: block.data, mimeType: block.mimeType }
|
|
117
|
+
)
|
|
118
|
+
return {
|
|
119
|
+
content: content.length > 0 ? content : [{ type: 'text', text: '(no output)' }],
|
|
120
|
+
isError: message.isError
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function imageBlock(block: ImageContent): CliBlock {
|
|
125
|
+
return { type: 'image', source: { type: 'base64', media_type: block.mimeType, data: block.data } }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function messageText(content: string | (TextContent | ImageContent)[]): string {
|
|
129
|
+
if (!Array.isArray(content)) return content
|
|
130
|
+
return content
|
|
131
|
+
.map((block) => (block.type === 'text' ? block.text : `[image: ${block.mimeType}]`))
|
|
132
|
+
.join('\n')
|
|
133
|
+
.trim()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function userContent(messages: TurnMessage[]): CliBlock[] {
|
|
137
|
+
const content: CliBlock[] = []
|
|
138
|
+
for (const message of messages) {
|
|
139
|
+
if (message.role === 'user') {
|
|
140
|
+
if (!Array.isArray(message.content)) {
|
|
141
|
+
if (message.content.length > 0) content.push({ type: 'text', text: message.content })
|
|
142
|
+
continue
|
|
143
|
+
}
|
|
144
|
+
for (const block of message.content) {
|
|
145
|
+
if (block.type === 'text') {
|
|
146
|
+
if (block.text.length > 0) content.push({ type: 'text', text: block.text })
|
|
147
|
+
} else {
|
|
148
|
+
content.push(imageBlock(block))
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
continue
|
|
152
|
+
}
|
|
153
|
+
if (message.role === 'system') {
|
|
154
|
+
const text = renderSystemMessageUpdate(message).trim()
|
|
155
|
+
if (text) content.push({ type: 'text', text: `<system-reminder>\n${text}\n</system-reminder>` })
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return content
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function replayContent(messages: TurnMessage[]): CliBlock[] {
|
|
162
|
+
if (messages.length === 1 && messages[0]?.role === 'user') return userContent(messages)
|
|
163
|
+
|
|
164
|
+
const parts: string[] = [
|
|
165
|
+
'This is the existing Pi conversation, replayed because the transport restarted. Treat it as prior turns, not as a new request. Continue after the final entry.'
|
|
166
|
+
]
|
|
167
|
+
for (const message of messages) {
|
|
168
|
+
if (message.role === 'user') {
|
|
169
|
+
const text = messageText(message.content)
|
|
170
|
+
if (text) parts.push(`USER:\n${text}`)
|
|
171
|
+
continue
|
|
172
|
+
}
|
|
173
|
+
if (message.role === 'toolResult') {
|
|
174
|
+
const text = messageText(message.content)
|
|
175
|
+
const status = message.isError ? 'failed' : 'ok'
|
|
176
|
+
parts.push(`TOOL RESULT ${message.toolName} (${status}):\n${text}`)
|
|
177
|
+
continue
|
|
178
|
+
}
|
|
179
|
+
if (message.role === 'system') {
|
|
180
|
+
const text = renderSystemMessageUpdate(message).trim()
|
|
181
|
+
if (text) parts.push(`SYSTEM UPDATE:\n${text}`)
|
|
182
|
+
continue
|
|
183
|
+
}
|
|
184
|
+
const rendered = message.content
|
|
185
|
+
.map((block) => {
|
|
186
|
+
if (block.type === 'text') return block.text
|
|
187
|
+
if (block.type === 'thinking') return ''
|
|
188
|
+
return `CALLED ${block.name}(${JSON.stringify(block.arguments)})`
|
|
189
|
+
})
|
|
190
|
+
.filter((line) => line.length > 0)
|
|
191
|
+
.join('\n')
|
|
192
|
+
if (rendered) parts.push(`ASSISTANT:\n${rendered}`)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const content: CliBlock[] = [{ type: 'text', text: parts.join('\n\n') }]
|
|
196
|
+
for (const message of messages) {
|
|
197
|
+
if (message.role !== 'user' || !Array.isArray(message.content)) continue
|
|
198
|
+
for (const block of message.content) {
|
|
199
|
+
if (block.type !== 'image') continue
|
|
200
|
+
content.push({ type: 'text', text: 'Image from an earlier user message:' })
|
|
201
|
+
content.push(imageBlock(block))
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return content
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function systemPreamble(cwd: string): string {
|
|
208
|
+
return [
|
|
209
|
+
'Your tools are MCP tools served by Pi. Call them by their declared names, which Claude Code shows as mcp__pi__<name>. No built-in tools are enabled.',
|
|
210
|
+
'Pi owns the conversation, approvals, file edits, shell, and compaction. Do not claim work you have not done with a Pi tool.',
|
|
211
|
+
`Working directory: ${cwd}`
|
|
212
|
+
].join('\n')
|
|
213
|
+
}
|