@meetopenbot/pi 0.0.2 → 0.1.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/README.md +11 -9
- package/dist/cloud-mode.js +10 -0
- package/dist/config.js +12 -9
- package/dist/credits-auth.js +90 -0
- package/dist/diff.js +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +167 -27
- package/dist/session.js +24 -5
- package/package.json +16 -9
package/README.md
CHANGED
|
@@ -22,13 +22,21 @@ plugins:
|
|
|
22
22
|
tools: read,bash,edit,write,grep,find,ls
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
## Auth
|
|
26
|
+
|
|
27
|
+
On cloud, `authMode: credits` (the default) uses your workspace credit balance via OpenBot for `openai`, `anthropic`, and `deepseek`. For BYOK, set `authMode: byok` and add the matching provider key under workspace settings (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `DEEPSEEK_API_KEY`, or `GEMINI_API_KEY`).
|
|
28
|
+
|
|
29
|
+
Locally, Pi always uses BYOK from environment variables or `~/.pi/agent/auth.json`.
|
|
30
|
+
|
|
31
|
+
Credits does not cover Google Gemini — Pi uses Gemini's native API, while OpenBot Credits proxies the OpenAI-compatible Gemini surface. Use openai/anthropic/deepseek on credits, or switch to BYOK for Gemini.
|
|
32
|
+
|
|
25
33
|
## Configuration
|
|
26
34
|
|
|
27
35
|
| Option | Description |
|
|
28
36
|
|--------|-------------|
|
|
29
|
-
| `
|
|
37
|
+
| `authMode` | Cloud only: `credits` (default) or `byok`. |
|
|
30
38
|
| `agentDir` | Pi config directory (credentials, settings, sessions). Default: `~/.pi/agent`. |
|
|
31
|
-
| `provider` | Model provider (e.g. `anthropic`, `openai`). |
|
|
39
|
+
| `provider` | Model provider (e.g. `anthropic`, `openai`, `deepseek`). |
|
|
32
40
|
| `model` | Model id (e.g. `claude-opus-4-5`). |
|
|
33
41
|
| `thinkingLevel` | Extended thinking: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`. |
|
|
34
42
|
| `tools` | Comma-separated built-in tools to enable. |
|
|
@@ -36,13 +44,7 @@ plugins:
|
|
|
36
44
|
| `noTools` | `all` or `builtin` to disable tools. |
|
|
37
45
|
| `systemPrompt` | Override Pi's system prompt for this agent. |
|
|
38
46
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
Pi resolves credentials via `AuthStorage` (see [Pi SDK docs](https://pi.dev/docs/latest/sdk)):
|
|
42
|
-
|
|
43
|
-
1. Runtime overrides
|
|
44
|
-
2. `~/.pi/agent/auth.json`
|
|
45
|
-
3. Environment variables (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, etc.)
|
|
47
|
+
Working directory always comes from the OpenBot channel `cwd`.
|
|
46
48
|
|
|
47
49
|
## Sessions
|
|
48
50
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** True when this runtime is a platform-managed cloud deployment. */
|
|
2
|
+
export const isCloudMode = () => process.env.OPENBOT_CLOUD_MODE === '1';
|
|
3
|
+
/** Default auth mode: Credits on cloud, BYOK locally. */
|
|
4
|
+
export const defaultAuthMode = () => (isCloudMode() ? 'credits' : 'byok');
|
|
5
|
+
export function resolveAuthMode(config) {
|
|
6
|
+
if (config.authMode === 'byok' || config.authMode === 'credits') {
|
|
7
|
+
return config.authMode;
|
|
8
|
+
}
|
|
9
|
+
return defaultAuthMode();
|
|
10
|
+
}
|
package/dist/config.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveAuthMode } from './cloud-mode.js';
|
|
1
2
|
const THINKING_LEVELS = new Set([
|
|
2
3
|
'off',
|
|
3
4
|
'minimal',
|
|
@@ -8,19 +9,21 @@ const THINKING_LEVELS = new Set([
|
|
|
8
9
|
]);
|
|
9
10
|
export const resolveConfig = (context, channelCwd) => {
|
|
10
11
|
const config = context.config;
|
|
11
|
-
const thinkingLevel = config.thinkingLevel && THINKING_LEVELS.has(config.thinkingLevel)
|
|
12
|
+
const thinkingLevel = typeof config.thinkingLevel === 'string' && THINKING_LEVELS.has(config.thinkingLevel)
|
|
12
13
|
? config.thinkingLevel
|
|
13
14
|
: undefined;
|
|
15
|
+
const asString = (value) => typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
14
16
|
return {
|
|
15
|
-
cwd: channelCwd ||
|
|
16
|
-
agentDir: config.agentDir,
|
|
17
|
-
provider: config.provider
|
|
18
|
-
model: config.model
|
|
17
|
+
cwd: channelCwd || process.cwd(),
|
|
18
|
+
agentDir: asString(config.agentDir),
|
|
19
|
+
provider: asString(config.provider),
|
|
20
|
+
model: asString(config.model),
|
|
19
21
|
thinkingLevel,
|
|
20
|
-
tools: config.tools
|
|
21
|
-
excludeTools: config.excludeTools
|
|
22
|
-
noTools: config.noTools,
|
|
23
|
-
systemPrompt: config.systemPrompt
|
|
22
|
+
tools: asString(config.tools),
|
|
23
|
+
excludeTools: asString(config.excludeTools),
|
|
24
|
+
noTools: config.noTools === 'all' || config.noTools === 'builtin' ? config.noTools : undefined,
|
|
25
|
+
systemPrompt: asString(config.systemPrompt),
|
|
26
|
+
authMode: resolveAuthMode(config),
|
|
24
27
|
};
|
|
25
28
|
};
|
|
26
29
|
export const parseToolList = (value) => {
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
export const INTEGRATIONS_TOKEN_HEADER = 'x-openbot-integrations-token';
|
|
2
|
+
export const CREDITS_API_KEY_PLACEHOLDER = 'openbot-credits';
|
|
3
|
+
/**
|
|
4
|
+
* Gateway paths matching each Pi provider's default base URL shape:
|
|
5
|
+
* - openai models use `https://api.openai.com/v1`
|
|
6
|
+
* - anthropic models use `https://api.anthropic.com` (SDK appends `/v1`)
|
|
7
|
+
* - deepseek is OpenAI-compat; `/v1` keeps metering on `/v1/chat/completions`
|
|
8
|
+
*
|
|
9
|
+
* Google Gemini is omitted: Pi speaks the native Generative Language API, while
|
|
10
|
+
* the credits gateway only proxies Gemini's OpenAI-compatible surface.
|
|
11
|
+
*/
|
|
12
|
+
export const CREDITS_PROVIDERS = [
|
|
13
|
+
{ id: 'openai', basePath: 'openai/v1', envVar: 'OPENAI_API_KEY' },
|
|
14
|
+
{ id: 'anthropic', basePath: 'anthropic', envVar: 'ANTHROPIC_API_KEY' },
|
|
15
|
+
{ id: 'deepseek', basePath: 'deepseek/v1', envVar: 'DEEPSEEK_API_KEY' },
|
|
16
|
+
];
|
|
17
|
+
const CREDITS_PROVIDER_IDS = new Set(CREDITS_PROVIDERS.map((provider) => provider.id));
|
|
18
|
+
export function isCreditsProvider(provider) {
|
|
19
|
+
return CREDITS_PROVIDER_IDS.has(provider);
|
|
20
|
+
}
|
|
21
|
+
/** Cloud host injects these when routing LLM calls through OpenBot Credits. */
|
|
22
|
+
export function resolveCreditsAuthConfig() {
|
|
23
|
+
const baseUrl = process.env.OPENBOT_INTEGRATIONS_BASE_URL?.trim();
|
|
24
|
+
const token = process.env.OPENBOT_INTEGRATIONS_TOKEN?.trim();
|
|
25
|
+
if (!baseUrl || !token)
|
|
26
|
+
return undefined;
|
|
27
|
+
return { baseUrl: baseUrl.replace(/\/$/, ''), token };
|
|
28
|
+
}
|
|
29
|
+
export function creditsProviderBaseUrl(config, provider) {
|
|
30
|
+
const match = CREDITS_PROVIDERS.find((entry) => entry.id === provider);
|
|
31
|
+
if (!match) {
|
|
32
|
+
throw new Error(`Unsupported credits provider: ${provider}`);
|
|
33
|
+
}
|
|
34
|
+
return `${config.baseUrl}/${match.basePath}`;
|
|
35
|
+
}
|
|
36
|
+
export function applyCreditsProviders(modelRegistry, authStorage, config) {
|
|
37
|
+
const apiKey = config.token || CREDITS_API_KEY_PLACEHOLDER;
|
|
38
|
+
const headers = { [INTEGRATIONS_TOKEN_HEADER]: config.token };
|
|
39
|
+
for (const provider of CREDITS_PROVIDERS) {
|
|
40
|
+
modelRegistry.registerProvider(provider.id, {
|
|
41
|
+
baseUrl: creditsProviderBaseUrl(config, provider.id),
|
|
42
|
+
headers,
|
|
43
|
+
});
|
|
44
|
+
authStorage.setRuntimeApiKey(provider.id, apiKey);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export function isCreditsErrorMessage(message) {
|
|
48
|
+
const lower = message.toLowerCase();
|
|
49
|
+
return (lower.includes('insufficient_credits') ||
|
|
50
|
+
lower.includes('insufficient credits') ||
|
|
51
|
+
lower.includes('402'));
|
|
52
|
+
}
|
|
53
|
+
export function isAuthErrorMessage(message) {
|
|
54
|
+
const lower = message.toLowerCase();
|
|
55
|
+
return (lower.includes('api key') ||
|
|
56
|
+
lower.includes('apikey') ||
|
|
57
|
+
lower.includes('401') ||
|
|
58
|
+
lower.includes('unauthorized') ||
|
|
59
|
+
lower.includes('authentication') ||
|
|
60
|
+
lower.includes('not logged in') ||
|
|
61
|
+
lower.includes('login'));
|
|
62
|
+
}
|
|
63
|
+
export function isIntegrationsProviderError(message) {
|
|
64
|
+
const lower = message.toLowerCase();
|
|
65
|
+
return (lower.includes('provider api key not configured') ||
|
|
66
|
+
(lower.includes('503') && lower.includes('provider')));
|
|
67
|
+
}
|
|
68
|
+
export const CREDITS_NOT_CONFIGURED_MESSAGE = 'OpenBot Credits is not configured on this runtime. The cloud host must set OPENBOT_INTEGRATIONS_BASE_URL and OPENBOT_INTEGRATIONS_TOKEN (try redeploying the workspace).';
|
|
69
|
+
export const CREDITS_PROVIDER_UNAVAILABLE_MESSAGE = 'OpenBot Credits could not reach the model provider — the platform provider API key is not configured yet. Try again later or switch this agent to BYOK mode.';
|
|
70
|
+
export const CREDITS_AUTH_FAILED_MESSAGE = 'Pi could not authenticate via OpenBot Credits. Check your workspace credit balance in settings, or switch this agent to BYOK mode.';
|
|
71
|
+
export function creditsErrorMessage(message) {
|
|
72
|
+
if (isIntegrationsProviderError(message)) {
|
|
73
|
+
return CREDITS_PROVIDER_UNAVAILABLE_MESSAGE;
|
|
74
|
+
}
|
|
75
|
+
if (isCreditsErrorMessage(message)) {
|
|
76
|
+
return 'Insufficient workspace credits. Add credits in workspace settings or switch this agent to BYOK mode.';
|
|
77
|
+
}
|
|
78
|
+
if (isAuthErrorMessage(message))
|
|
79
|
+
return CREDITS_AUTH_FAILED_MESSAGE;
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
export function remapPiError(message, authMode) {
|
|
83
|
+
if (authMode !== 'credits')
|
|
84
|
+
return message;
|
|
85
|
+
return creditsErrorMessage(message) ?? message;
|
|
86
|
+
}
|
|
87
|
+
export function unsupportedCreditsProviderMessage(provider) {
|
|
88
|
+
return (`OpenBot Credits does not support the "${provider}" provider. ` +
|
|
89
|
+
'Use openai, anthropic, or deepseek, or switch this agent to BYOK mode.');
|
|
90
|
+
}
|
package/dist/diff.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { buildDiffWidget as runDiffWidget, diffFileFromMutationTool as diffFileFromPiTool, } from '@meetopenbot/plugin-sdk';
|
package/dist/index.d.ts
ADDED
package/dist/index.js
CHANGED
|
@@ -1,8 +1,64 @@
|
|
|
1
|
-
import { agentOutput, definePlugin, shouldHandleInvoke, uiWidget, } from '@meetopenbot/plugin-sdk';
|
|
1
|
+
import { agentOutput, buildDiffWidget, definePlugin, diffFileFromMutationTool, resolveRunDiffFiles, shouldHandleInvoke, snapshotWorkspace, toolTraceWidget, uiWidget, } from '@meetopenbot/plugin-sdk';
|
|
2
|
+
import { isCloudMode } from './cloud-mode.js';
|
|
2
3
|
import { resolveConfig } from './config.js';
|
|
4
|
+
import { isAuthErrorMessage, remapPiError, } from './credits-auth.js';
|
|
3
5
|
import { formatPiError, formatToolResult } from './format.js';
|
|
4
|
-
import { getOrCreatePiSession } from './session.js';
|
|
6
|
+
import { disposePiSessionsForThread, getOrCreatePiSession } from './session.js';
|
|
5
7
|
import { streamPiPrompt } from './stream.js';
|
|
8
|
+
const BYOK_PROVIDERS = {
|
|
9
|
+
anthropic: {
|
|
10
|
+
envVar: 'ANTHROPIC_API_KEY',
|
|
11
|
+
label: 'Anthropic',
|
|
12
|
+
placeholder: 'sk-ant-...',
|
|
13
|
+
},
|
|
14
|
+
openai: {
|
|
15
|
+
envVar: 'OPENAI_API_KEY',
|
|
16
|
+
label: 'OpenAI',
|
|
17
|
+
placeholder: 'sk-...',
|
|
18
|
+
},
|
|
19
|
+
deepseek: {
|
|
20
|
+
envVar: 'DEEPSEEK_API_KEY',
|
|
21
|
+
label: 'DeepSeek',
|
|
22
|
+
placeholder: 'sk-...',
|
|
23
|
+
},
|
|
24
|
+
google: {
|
|
25
|
+
envVar: 'GEMINI_API_KEY',
|
|
26
|
+
label: 'Google Gemini',
|
|
27
|
+
placeholder: 'AIza...',
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
const resolveByokProvider = (provider) => BYOK_PROVIDERS[provider ?? ''] ?? BYOK_PROVIDERS.openai;
|
|
31
|
+
const buildApiKeyWidget = (agentId, threadId, reason, provider) => {
|
|
32
|
+
const byok = resolveByokProvider(provider);
|
|
33
|
+
return uiWidget({
|
|
34
|
+
agentId,
|
|
35
|
+
threadId,
|
|
36
|
+
widget: {
|
|
37
|
+
kind: 'form',
|
|
38
|
+
widgetId: `pi_api_key_request_${Date.now()}`,
|
|
39
|
+
title: `${byok.label} API Key Required`,
|
|
40
|
+
description: `Pi could not authenticate (${reason}). ` +
|
|
41
|
+
`Provide a ${byok.label} API key to continue. ` +
|
|
42
|
+
'The key is stored as a workspace variable on your machine and never leaves your local runtime.',
|
|
43
|
+
fields: [
|
|
44
|
+
{
|
|
45
|
+
id: 'apiKey',
|
|
46
|
+
label: 'API Key',
|
|
47
|
+
type: 'password',
|
|
48
|
+
placeholder: byok.placeholder,
|
|
49
|
+
required: true,
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
submitLabel: 'Save API Key',
|
|
53
|
+
metadata: {
|
|
54
|
+
type: 'api_key_request',
|
|
55
|
+
provider: provider && BYOK_PROVIDERS[provider] ? provider : 'openai',
|
|
56
|
+
envVar: byok.envVar,
|
|
57
|
+
source: 'pi',
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
};
|
|
6
62
|
export default definePlugin({
|
|
7
63
|
id: 'pi',
|
|
8
64
|
name: 'Pi',
|
|
@@ -10,17 +66,23 @@ export default definePlugin({
|
|
|
10
66
|
configSchema: {
|
|
11
67
|
type: 'object',
|
|
12
68
|
properties: {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
69
|
+
...(isCloudMode()
|
|
70
|
+
? {
|
|
71
|
+
authMode: {
|
|
72
|
+
type: 'string',
|
|
73
|
+
description: 'Credits — use your workspace credit balance via OpenBot. BYOK — bring your own provider API key.',
|
|
74
|
+
enum: ['credits', 'byok'],
|
|
75
|
+
default: 'credits',
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
: {}),
|
|
17
79
|
agentDir: {
|
|
18
80
|
type: 'string',
|
|
19
81
|
description: 'Pi config directory (default: ~/.pi/agent).',
|
|
20
82
|
},
|
|
21
83
|
provider: {
|
|
22
84
|
type: 'string',
|
|
23
|
-
description: 'Model provider (e.g. anthropic, openai).',
|
|
85
|
+
description: 'Model provider (e.g. anthropic, openai, deepseek).',
|
|
24
86
|
},
|
|
25
87
|
model: {
|
|
26
88
|
type: 'string',
|
|
@@ -67,6 +129,17 @@ export default definePlugin({
|
|
|
67
129
|
}
|
|
68
130
|
const channelCwd = handlerCtx.state.channelDetails?.cwd;
|
|
69
131
|
const config = resolveConfig(context, channelCwd);
|
|
132
|
+
const fail = function* (message) {
|
|
133
|
+
const mapped = remapPiError(message, config.authMode);
|
|
134
|
+
if (config.authMode === 'byok' && isAuthErrorMessage(message)) {
|
|
135
|
+
yield buildApiKeyWidget(context.agentId, threadId, mapped, config.provider);
|
|
136
|
+
}
|
|
137
|
+
yield agentOutput({
|
|
138
|
+
agentId: context.agentId,
|
|
139
|
+
content: `**Pi error:** ${mapped}`,
|
|
140
|
+
threadId,
|
|
141
|
+
});
|
|
142
|
+
};
|
|
70
143
|
try {
|
|
71
144
|
const { session } = await getOrCreatePiSession({
|
|
72
145
|
config,
|
|
@@ -75,6 +148,8 @@ export default definePlugin({
|
|
|
75
148
|
});
|
|
76
149
|
let fullTextContent = '';
|
|
77
150
|
const toolInfoMap = new Map();
|
|
151
|
+
const changedFiles = new Map();
|
|
152
|
+
const snapshot = snapshotWorkspace(config.cwd);
|
|
78
153
|
for await (const chunk of streamPiPrompt(session, prompt, {
|
|
79
154
|
streaming: session.isStreaming,
|
|
80
155
|
})) {
|
|
@@ -87,14 +162,12 @@ export default definePlugin({
|
|
|
87
162
|
yield uiWidget({
|
|
88
163
|
agentId: context.agentId,
|
|
89
164
|
threadId,
|
|
90
|
-
widget: {
|
|
91
|
-
kind: 'message',
|
|
165
|
+
widget: toolTraceWidget({
|
|
92
166
|
widgetId: chunk.toolCallId,
|
|
167
|
+
groupId: 'pi:tools',
|
|
93
168
|
title: chunk.statusLine,
|
|
94
169
|
body: `**Input**\n\`\`\`json\n${JSON.stringify(chunk.args, null, 2)}\n\`\`\``,
|
|
95
|
-
|
|
96
|
-
display: "collapsed",
|
|
97
|
-
},
|
|
170
|
+
}),
|
|
98
171
|
});
|
|
99
172
|
break;
|
|
100
173
|
case 'tool_end': {
|
|
@@ -106,37 +179,46 @@ export default definePlugin({
|
|
|
106
179
|
yield uiWidget({
|
|
107
180
|
agentId: context.agentId,
|
|
108
181
|
threadId,
|
|
109
|
-
widget: {
|
|
110
|
-
kind: 'message',
|
|
182
|
+
widget: toolTraceWidget({
|
|
111
183
|
widgetId: chunk.toolCallId,
|
|
184
|
+
groupId: 'pi:tools',
|
|
112
185
|
title: chunk.statusLine || info?.statusLine || `Tool ${chunk.toolName} finished`,
|
|
113
186
|
body: inputMd + outputMd,
|
|
114
|
-
variant: 'basic',
|
|
115
|
-
display: "collapsed",
|
|
116
187
|
state: chunk.isError ? 'error' : 'submitted',
|
|
117
|
-
},
|
|
188
|
+
}),
|
|
118
189
|
});
|
|
190
|
+
if (!chunk.isError) {
|
|
191
|
+
const file = diffFileFromMutationTool({
|
|
192
|
+
toolName: chunk.toolName,
|
|
193
|
+
input: info?.args,
|
|
194
|
+
result: chunk.result,
|
|
195
|
+
});
|
|
196
|
+
if (file)
|
|
197
|
+
changedFiles.set(file.path, file);
|
|
198
|
+
}
|
|
119
199
|
break;
|
|
120
200
|
}
|
|
121
201
|
case 'status':
|
|
122
202
|
yield uiWidget({
|
|
123
203
|
agentId: context.agentId,
|
|
124
204
|
threadId,
|
|
125
|
-
widget: {
|
|
126
|
-
kind: 'message',
|
|
205
|
+
widget: toolTraceWidget({
|
|
127
206
|
widgetId: `status-${Date.now()}`,
|
|
128
|
-
|
|
207
|
+
groupId: 'pi:tools',
|
|
208
|
+
title: 'Status',
|
|
129
209
|
body: chunk.statusLine,
|
|
130
|
-
|
|
131
|
-
display: "collapsed",
|
|
132
|
-
},
|
|
210
|
+
}),
|
|
133
211
|
});
|
|
134
212
|
break;
|
|
135
|
-
case 'text':
|
|
213
|
+
case 'text': {
|
|
214
|
+
const content = chunk.content.startsWith('**Pi error:** ')
|
|
215
|
+
? `**Pi error:** ${remapPiError(chunk.content.slice('**Pi error:** '.length), config.authMode)}`
|
|
216
|
+
: chunk.content;
|
|
136
217
|
if (fullTextContent)
|
|
137
218
|
fullTextContent += '\n\n';
|
|
138
|
-
fullTextContent +=
|
|
219
|
+
fullTextContent += content;
|
|
139
220
|
break;
|
|
221
|
+
}
|
|
140
222
|
}
|
|
141
223
|
}
|
|
142
224
|
if (fullTextContent.trim()) {
|
|
@@ -146,12 +228,70 @@ export default definePlugin({
|
|
|
146
228
|
threadId,
|
|
147
229
|
});
|
|
148
230
|
}
|
|
231
|
+
const diff = buildDiffWidget({
|
|
232
|
+
widgetId: `pi-diff:${threadId ?? 'run'}:${Date.now()}`,
|
|
233
|
+
files: resolveRunDiffFiles({
|
|
234
|
+
snapshot,
|
|
235
|
+
fallback: changedFiles.values(),
|
|
236
|
+
}),
|
|
237
|
+
});
|
|
238
|
+
if (diff) {
|
|
239
|
+
yield uiWidget({
|
|
240
|
+
agentId: context.agentId,
|
|
241
|
+
threadId,
|
|
242
|
+
widget: diff,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
149
245
|
}
|
|
150
246
|
catch (error) {
|
|
247
|
+
yield* fail(formatPiError(error));
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
builder.on('client:ui:widget:response', async function* (event, handlerCtx) {
|
|
251
|
+
const { metadata, values, widgetId } = event.data ?? {};
|
|
252
|
+
if (!metadata || metadata.type !== 'api_key_request')
|
|
253
|
+
return;
|
|
254
|
+
if (metadata.source !== 'pi')
|
|
255
|
+
return;
|
|
256
|
+
const apiKey = values?.apiKey;
|
|
257
|
+
if (typeof apiKey !== 'string' || !apiKey)
|
|
258
|
+
return;
|
|
259
|
+
const envVar = typeof metadata.envVar === 'string' ? metadata.envVar : 'OPENAI_API_KEY';
|
|
260
|
+
const storage = context.storage;
|
|
261
|
+
if (!storage) {
|
|
151
262
|
yield agentOutput({
|
|
152
263
|
agentId: context.agentId,
|
|
153
|
-
content:
|
|
154
|
-
threadId,
|
|
264
|
+
content: '[pi] no storage available; cannot persist API key.',
|
|
265
|
+
threadId: handlerCtx.state.threadId,
|
|
266
|
+
});
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
try {
|
|
270
|
+
await storage.createVariable({ key: envVar, value: apiKey, secret: true });
|
|
271
|
+
process.env[envVar] = apiKey;
|
|
272
|
+
disposePiSessionsForThread(handlerCtx.state);
|
|
273
|
+
yield uiWidget({
|
|
274
|
+
agentId: context.agentId,
|
|
275
|
+
widget: {
|
|
276
|
+
widgetId: widgetId ?? `pi_api_key_saved_${Date.now()}`,
|
|
277
|
+
kind: 'message',
|
|
278
|
+
title: 'API Key Saved',
|
|
279
|
+
body: `Saved ${envVar} as a workspace variable. You can now continue the conversation.`,
|
|
280
|
+
state: 'submitted',
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
yield agentOutput({
|
|
284
|
+
agentId: context.agentId,
|
|
285
|
+
content: `Saved ${envVar} to workspace variables. Re-send your last message to retry.`,
|
|
286
|
+
threadId: handlerCtx.state.threadId,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
291
|
+
yield agentOutput({
|
|
292
|
+
agentId: context.agentId,
|
|
293
|
+
content: `[pi] failed to save API key: ${errorMessage}`,
|
|
294
|
+
threadId: handlerCtx.state.threadId,
|
|
155
295
|
});
|
|
156
296
|
}
|
|
157
297
|
});
|
package/dist/session.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { AuthStorage, createAgentSession, DefaultResourceLoader, getAgentDir, ModelRegistry, SessionManager, SettingsManager, } from '@earendil-works/pi-coding-agent';
|
|
2
2
|
import { parseToolList } from './config.js';
|
|
3
|
+
import { applyCreditsProviders, CREDITS_NOT_CONFIGURED_MESSAGE, isCreditsProvider, resolveCreditsAuthConfig, unsupportedCreditsProviderMessage, } from './credits-auth.js';
|
|
3
4
|
import { persistPiState, readPersistedState } from './state.js';
|
|
4
5
|
const sessionCache = new Map();
|
|
5
|
-
const buildSessionKey = (state) =>
|
|
6
|
+
const buildSessionKey = (state, config) => {
|
|
7
|
+
const scope = state.threadId ? `${state.channelId}:${state.threadId}` : state.channelId;
|
|
8
|
+
return `${scope}:${config.authMode}`;
|
|
9
|
+
};
|
|
6
10
|
const resolveModel = async (config, modelRegistry) => {
|
|
7
11
|
if (config.provider && config.model) {
|
|
8
12
|
return modelRegistry.find(config.provider, config.model) ?? undefined;
|
|
@@ -12,24 +16,32 @@ const resolveModel = async (config, modelRegistry) => {
|
|
|
12
16
|
};
|
|
13
17
|
export const getOrCreatePiSession = async (args) => {
|
|
14
18
|
const { config, state, storage } = args;
|
|
15
|
-
const sessionKey = buildSessionKey(state);
|
|
19
|
+
const sessionKey = buildSessionKey(state, config);
|
|
16
20
|
const cached = sessionCache.get(sessionKey);
|
|
17
21
|
if (cached)
|
|
18
22
|
return cached;
|
|
23
|
+
if (config.authMode === 'credits' && config.provider && !isCreditsProvider(config.provider)) {
|
|
24
|
+
throw new Error(unsupportedCreditsProviderMessage(config.provider));
|
|
25
|
+
}
|
|
19
26
|
const cwd = config.cwd || process.cwd();
|
|
20
27
|
const agentDir = config.agentDir || getAgentDir();
|
|
21
28
|
const persisted = readPersistedState(state);
|
|
22
29
|
const authStorage = AuthStorage.create(`${agentDir}/auth.json`);
|
|
23
30
|
const modelRegistry = ModelRegistry.create(authStorage, `${agentDir}/models.json`);
|
|
31
|
+
if (config.authMode === 'credits') {
|
|
32
|
+
const credits = resolveCreditsAuthConfig();
|
|
33
|
+
if (!credits) {
|
|
34
|
+
throw new Error(CREDITS_NOT_CONFIGURED_MESSAGE);
|
|
35
|
+
}
|
|
36
|
+
applyCreditsProviders(modelRegistry, authStorage, credits);
|
|
37
|
+
}
|
|
24
38
|
const model = await resolveModel(config, modelRegistry);
|
|
25
39
|
const settingsManager = SettingsManager.create(cwd, agentDir);
|
|
26
40
|
const loader = new DefaultResourceLoader({
|
|
27
41
|
cwd,
|
|
28
42
|
agentDir,
|
|
29
43
|
settingsManager,
|
|
30
|
-
...(config.systemPrompt
|
|
31
|
-
? { systemPromptOverride: () => config.systemPrompt }
|
|
32
|
-
: {}),
|
|
44
|
+
...(config.systemPrompt ? { systemPromptOverride: () => config.systemPrompt } : {}),
|
|
33
45
|
});
|
|
34
46
|
await loader.reload();
|
|
35
47
|
const sessionManager = persisted.sessionFile
|
|
@@ -66,3 +78,10 @@ export const disposePiSession = (sessionKey) => {
|
|
|
66
78
|
cached.session.dispose();
|
|
67
79
|
sessionCache.delete(sessionKey);
|
|
68
80
|
};
|
|
81
|
+
export const disposePiSessionsForThread = (state) => {
|
|
82
|
+
const prefix = state.threadId ? `${state.channelId}:${state.threadId}:` : `${state.channelId}:`;
|
|
83
|
+
for (const key of sessionCache.keys()) {
|
|
84
|
+
if (key.startsWith(prefix))
|
|
85
|
+
disposePiSession(key);
|
|
86
|
+
}
|
|
87
|
+
};
|
package/package.json
CHANGED
|
@@ -1,30 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meetopenbot/pi",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "OpenBot agent plugin powered by the Pi coding agent SDK.",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "dist/index.js",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
7
|
"publishConfig": {
|
|
8
8
|
"access": "public"
|
|
9
9
|
},
|
|
10
10
|
"exports": {
|
|
11
|
-
".":
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
}
|
|
12
16
|
},
|
|
13
17
|
"files": [
|
|
14
18
|
"dist"
|
|
15
19
|
],
|
|
16
|
-
"scripts": {
|
|
17
|
-
"build": "tsc -p tsconfig.json",
|
|
18
|
-
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
19
|
-
},
|
|
20
20
|
"license": "MIT",
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@earendil-works/pi-ai": "^0.79.1",
|
|
23
23
|
"@earendil-works/pi-coding-agent": "^0.79.1",
|
|
24
|
-
"@meetopenbot/plugin-sdk": "^0.
|
|
24
|
+
"@meetopenbot/plugin-sdk": "^0.2.0"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@types/node": "^25.9.2",
|
|
28
28
|
"typescript": "^6.0.3"
|
|
29
|
+
},
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsc -p tsconfig.json && node ../../scripts/write-plugin-declaration.mjs",
|
|
33
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
34
|
+
"test": "node --experimental-strip-types --test src/*.test.ts",
|
|
35
|
+
"dev": "tsc -p tsconfig.json --watch --preserveWatchOutput"
|
|
29
36
|
}
|
|
30
|
-
}
|
|
37
|
+
}
|