@meetopenbot/pi 0.0.3 → 0.1.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/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
- | `cwd` | Working directory for Pi tools and resource discovery. Defaults to the OpenBot channel `cwd`, then `process.cwd()`. |
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
- ## API Keys
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
 
package/dist/config.js CHANGED
@@ -1,34 +1,11 @@
1
- const THINKING_LEVELS = new Set([
2
- 'off',
3
- 'minimal',
4
- 'low',
5
- 'medium',
6
- 'high',
7
- 'xhigh',
8
- ]);
1
+ import { trimmedString, vendorModelId } from '@meetopenbot/plugin-sdk';
9
2
  export const resolveConfig = (context, channelCwd) => {
10
- const config = context.config;
11
- const thinkingLevel = config.thinkingLevel && THINKING_LEVELS.has(config.thinkingLevel)
12
- ? config.thinkingLevel
13
- : undefined;
3
+ const raw = trimmedString(context.config.model);
4
+ const provider = raw?.includes('/') ? raw.split('/')[0] : undefined;
5
+ const model = vendorModelId(raw);
14
6
  return {
15
- cwd: channelCwd || config.cwd,
16
- agentDir: config.agentDir,
17
- provider: config.provider?.trim() || undefined,
18
- model: config.model?.trim() || undefined,
19
- thinkingLevel,
20
- tools: config.tools?.trim() || undefined,
21
- excludeTools: config.excludeTools?.trim() || undefined,
22
- noTools: config.noTools,
23
- systemPrompt: config.systemPrompt?.trim() || undefined,
7
+ cwd: channelCwd || process.cwd(),
8
+ provider,
9
+ model,
24
10
  };
25
11
  };
26
- export const parseToolList = (value) => {
27
- if (!value)
28
- return undefined;
29
- const tools = value
30
- .split(',')
31
- .map((tool) => tool.trim())
32
- .filter(Boolean);
33
- return tools.length > 0 ? tools : undefined;
34
- };
@@ -0,0 +1,51 @@
1
+ import { CREDITS_API_KEY_PLACEHOLDER, CREDITS_NOT_CONFIGURED_MESSAGE, INTEGRATIONS_TOKEN_HEADER, creditsErrorMessage as mapCreditsError, creditsAuthFailedMessage, creditsProviderBaseUrl as sdkCreditsProviderBaseUrl, isAuthErrorMessage, llmAuthNotConfiguredMessage, resolveByokApiKey, resolveCreditsAuthConfig, } from '@meetopenbot/plugin-sdk';
2
+ export { CREDITS_API_KEY_PLACEHOLDER, CREDITS_NOT_CONFIGURED_MESSAGE, INTEGRATIONS_TOKEN_HEADER, isAuthErrorMessage, llmAuthNotConfiguredMessage, resolveByokApiKey, resolveCreditsAuthConfig, };
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
+ const PI_CREDITS_PATHS = Object.fromEntries(CREDITS_PROVIDERS.map((provider) => [provider.id, provider.basePath]));
19
+ export function isCreditsProvider(provider) {
20
+ return CREDITS_PROVIDER_IDS.has(provider);
21
+ }
22
+ export function creditsProviderBaseUrl(config, provider) {
23
+ return sdkCreditsProviderBaseUrl(config, provider, PI_CREDITS_PATHS);
24
+ }
25
+ export function applyCreditsProviders(modelRegistry, authStorage, config) {
26
+ const apiKey = config.token || CREDITS_API_KEY_PLACEHOLDER;
27
+ const headers = { [INTEGRATIONS_TOKEN_HEADER]: config.token };
28
+ for (const provider of CREDITS_PROVIDERS) {
29
+ modelRegistry.registerProvider(provider.id, {
30
+ baseUrl: creditsProviderBaseUrl(config, provider.id),
31
+ headers,
32
+ });
33
+ authStorage.setRuntimeApiKey(provider.id, apiKey);
34
+ }
35
+ }
36
+ export const CREDITS_AUTH_FAILED_MESSAGE = creditsAuthFailedMessage('Pi');
37
+ export function creditsErrorMessage(message) {
38
+ return mapCreditsError(message, { agentName: 'Pi' });
39
+ }
40
+ export function remapPiError(message) {
41
+ if (resolveByokApiKey('openai') ||
42
+ resolveByokApiKey('anthropic') ||
43
+ resolveByokApiKey('google') ||
44
+ resolveByokApiKey('deepseek')) {
45
+ return message;
46
+ }
47
+ return creditsErrorMessage(message) ?? message;
48
+ }
49
+ export function unsupportedCreditsProviderMessage(provider) {
50
+ return `OpenBot Credits does not support the "${provider}" provider. Use openai, anthropic, or deepseek.`;
51
+ }
package/dist/index.js CHANGED
@@ -1,178 +1,18 @@
1
- import { agentOutput, buildDiffWidget, definePlugin, diffFileFromMutationTool, resolveRunDiffFiles, shouldHandleInvoke, snapshotWorkspace, toolTraceWidget, uiWidget, } from '@meetopenbot/plugin-sdk';
2
- import { resolveConfig } from './config.js';
3
- import { formatPiError, formatToolResult } from './format.js';
4
- import { getOrCreatePiSession } from './session.js';
5
- import { streamPiPrompt } from './stream.js';
6
- export default definePlugin({
7
- id: 'pi',
1
+ import { defineAgentPlugin } from '@meetopenbot/plugin-sdk';
2
+ import { remapPiError } from './credits.js';
3
+ import { runPiTurn } from './runtime.js';
4
+ export const plugin = await defineAgentPlugin({
8
5
  name: 'Pi',
9
6
  description: 'Pi coding agent — read, edit, and run code in your workspace.',
10
- configSchema: {
11
- type: 'object',
12
- properties: {
13
- cwd: {
14
- type: 'string',
15
- description: 'Working directory for Pi tools and resource discovery.',
16
- },
17
- agentDir: {
18
- type: 'string',
19
- description: 'Pi config directory (default: ~/.pi/agent).',
20
- },
21
- provider: {
22
- type: 'string',
23
- description: 'Model provider (e.g. anthropic, openai).',
24
- },
25
- model: {
26
- type: 'string',
27
- description: 'Model id (e.g. claude-opus-4-5).',
28
- },
29
- thinkingLevel: {
30
- type: 'string',
31
- description: 'Extended thinking level.',
32
- enum: ['off', 'minimal', 'low', 'medium', 'high', 'xhigh'],
33
- default: 'off',
34
- },
35
- tools: {
36
- type: 'string',
37
- description: 'Comma-separated built-in tools to enable (e.g. read,bash,edit,write,grep,find,ls).',
38
- },
39
- excludeTools: {
40
- type: 'string',
41
- description: 'Comma-separated tool names to disable.',
42
- },
43
- noTools: {
44
- type: 'string',
45
- description: 'Disable tools: "all" or "builtin".',
46
- enum: ['all', 'builtin'],
47
- },
48
- systemPrompt: {
49
- type: 'string',
50
- description: 'Override Pi system prompt for this agent.',
51
- },
52
- },
7
+ models: {
8
+ providers: ['openai', 'anthropic', 'deepseek'],
9
+ default: 'openai/gpt-5.6-luna',
53
10
  },
54
- factory: (context) => (builder) => {
55
- builder.on('agent:invoke', async function* (event, handlerCtx) {
56
- if (!shouldHandleInvoke(event, context.agentId))
57
- return;
58
- const threadId = event.meta?.threadId ?? handlerCtx.state.threadId;
59
- const prompt = (event.data.content ?? '').trim();
60
- if (!prompt) {
61
- yield agentOutput({
62
- agentId: context.agentId,
63
- content: 'Send a message to run Pi in this workspace — for example, "List the files here" or "Fix the failing test in src/foo.test.ts".',
64
- threadId,
65
- });
66
- return;
67
- }
68
- const channelCwd = handlerCtx.state.channelDetails?.cwd;
69
- const config = resolveConfig(context, channelCwd);
70
- try {
71
- const { session } = await getOrCreatePiSession({
72
- config,
73
- state: handlerCtx.state,
74
- storage: context.storage,
75
- });
76
- let fullTextContent = '';
77
- const toolInfoMap = new Map();
78
- const changedFiles = new Map();
79
- const snapshot = snapshotWorkspace(config.cwd);
80
- for await (const chunk of streamPiPrompt(session, prompt, {
81
- streaming: session.isStreaming,
82
- })) {
83
- switch (chunk.type) {
84
- case 'tool_start':
85
- toolInfoMap.set(chunk.toolCallId, {
86
- statusLine: chunk.statusLine,
87
- args: chunk.args,
88
- });
89
- yield uiWidget({
90
- agentId: context.agentId,
91
- threadId,
92
- widget: toolTraceWidget({
93
- widgetId: chunk.toolCallId,
94
- groupId: 'pi:tools',
95
- title: chunk.statusLine,
96
- body: `**Input**\n\`\`\`json\n${JSON.stringify(chunk.args, null, 2)}\n\`\`\``,
97
- }),
98
- });
99
- break;
100
- case 'tool_end': {
101
- const info = toolInfoMap.get(chunk.toolCallId);
102
- const inputMd = info
103
- ? `**Input**\n\`\`\`json\n${JSON.stringify(info.args, null, 2)}\n\`\`\`\n\n`
104
- : '';
105
- const outputMd = `**Output**\n${formatToolResult(chunk.result)}`;
106
- yield uiWidget({
107
- agentId: context.agentId,
108
- threadId,
109
- widget: toolTraceWidget({
110
- widgetId: chunk.toolCallId,
111
- groupId: 'pi:tools',
112
- title: chunk.statusLine || info?.statusLine || `Tool ${chunk.toolName} finished`,
113
- body: inputMd + outputMd,
114
- state: chunk.isError ? 'error' : 'submitted',
115
- }),
116
- });
117
- if (!chunk.isError) {
118
- const file = diffFileFromMutationTool({
119
- toolName: chunk.toolName,
120
- input: info?.args,
121
- result: chunk.result,
122
- });
123
- if (file)
124
- changedFiles.set(file.path, file);
125
- }
126
- break;
127
- }
128
- case 'status':
129
- yield uiWidget({
130
- agentId: context.agentId,
131
- threadId,
132
- widget: toolTraceWidget({
133
- widgetId: `status-${Date.now()}`,
134
- groupId: 'pi:tools',
135
- title: 'Status',
136
- body: chunk.statusLine,
137
- }),
138
- });
139
- break;
140
- case 'text':
141
- if (fullTextContent)
142
- fullTextContent += '\n\n';
143
- fullTextContent += chunk.content;
144
- break;
145
- }
146
- }
147
- if (fullTextContent.trim()) {
148
- yield agentOutput({
149
- agentId: context.agentId,
150
- content: fullTextContent.trim(),
151
- threadId,
152
- });
153
- }
154
- const diff = buildDiffWidget({
155
- widgetId: `pi-diff:${threadId ?? 'run'}:${Date.now()}`,
156
- files: resolveRunDiffFiles({
157
- snapshot,
158
- fallback: changedFiles.values(),
159
- }),
160
- });
161
- if (diff) {
162
- yield uiWidget({
163
- agentId: context.agentId,
164
- threadId,
165
- widget: diff,
166
- });
167
- }
168
- }
169
- catch (error) {
170
- yield agentOutput({
171
- agentId: context.agentId,
172
- content: `**Pi error:** ${formatPiError(error)}`,
173
- threadId,
174
- });
175
- }
176
- });
11
+ emptyPrompt: 'Send a message to run Pi in this workspace — for example, "List the files here" or "Fix the failing test in src/foo.test.ts".',
12
+ run: runPiTurn,
13
+ mapError: (error) => {
14
+ const message = error instanceof Error ? error.message : String(error);
15
+ return { kind: 'reply', content: `Pi error: ${remapPiError(message)}` };
177
16
  },
178
17
  });
18
+ export default plugin;
@@ -0,0 +1,17 @@
1
+ import { resolveConfig } from './config.js';
2
+ import { remapPiError } from './credits.js';
3
+ import { getOrCreatePiSession } from './session.js';
4
+ import { runPiPrompt } from './stream.js';
5
+ export async function runPiTurn({ prompt, handlerCtx, context }) {
6
+ const config = resolveConfig(context, handlerCtx.state.channelDetails?.cwd);
7
+ const { session } = await getOrCreatePiSession({
8
+ config,
9
+ state: handlerCtx.state,
10
+ storage: context.storage,
11
+ });
12
+ const text = await runPiPrompt(session, prompt, { streaming: session.isStreaming });
13
+ if (text.startsWith('Pi error: ')) {
14
+ return `Pi error: ${remapPiError(text.slice('Pi error: '.length))}`;
15
+ }
16
+ return text;
17
+ }
package/dist/session.js CHANGED
@@ -1,8 +1,20 @@
1
1
  import { AuthStorage, createAgentSession, DefaultResourceLoader, getAgentDir, ModelRegistry, SessionManager, SettingsManager, } from '@earendil-works/pi-coding-agent';
2
- import { parseToolList } from './config.js';
2
+ import { isCloudMode } from '@meetopenbot/plugin-sdk';
3
+ import { applyCreditsProviders, isCreditsProvider, llmAuthNotConfiguredMessage, resolveByokApiKey, resolveCreditsAuthConfig, unsupportedCreditsProviderMessage, } from './credits.js';
3
4
  import { persistPiState, readPersistedState } from './state.js';
4
5
  const sessionCache = new Map();
5
- const buildSessionKey = (state) => state.threadId ? `${state.channelId}:${state.threadId}` : state.channelId;
6
+ const hasPiByok = (provider) => {
7
+ if (provider)
8
+ return Boolean(resolveByokApiKey(provider));
9
+ return Boolean(resolveByokApiKey('openai') ||
10
+ resolveByokApiKey('anthropic') ||
11
+ resolveByokApiKey('google') ||
12
+ resolveByokApiKey('deepseek'));
13
+ };
14
+ const buildSessionKey = (state, byok) => {
15
+ const scope = state.threadId ? `${state.channelId}:${state.threadId}` : state.channelId;
16
+ return `${scope}:${byok ? 'byok' : 'credits'}`;
17
+ };
6
18
  const resolveModel = async (config, modelRegistry) => {
7
19
  if (config.provider && config.model) {
8
20
  return modelRegistry.find(config.provider, config.model) ?? undefined;
@@ -12,24 +24,32 @@ const resolveModel = async (config, modelRegistry) => {
12
24
  };
13
25
  export const getOrCreatePiSession = async (args) => {
14
26
  const { config, state, storage } = args;
15
- const sessionKey = buildSessionKey(state);
27
+ const byok = hasPiByok(config.provider);
28
+ const sessionKey = buildSessionKey(state, byok);
16
29
  const cached = sessionCache.get(sessionKey);
17
30
  if (cached)
18
31
  return cached;
32
+ if (!byok && config.provider && !isCreditsProvider(config.provider)) {
33
+ throw new Error(unsupportedCreditsProviderMessage(config.provider));
34
+ }
19
35
  const cwd = config.cwd || process.cwd();
20
- const agentDir = config.agentDir || getAgentDir();
36
+ const agentDir = getAgentDir();
21
37
  const persisted = readPersistedState(state);
22
38
  const authStorage = AuthStorage.create(`${agentDir}/auth.json`);
23
39
  const modelRegistry = ModelRegistry.create(authStorage, `${agentDir}/models.json`);
40
+ const credits = !byok && isCloudMode() ? resolveCreditsAuthConfig() : undefined;
41
+ if (!byok && !credits) {
42
+ throw new Error(llmAuthNotConfiguredMessage(config.provider === 'anthropic' ? 'ANTHROPIC_API_KEY' : 'OPENAI_API_KEY'));
43
+ }
44
+ if (credits) {
45
+ applyCreditsProviders(modelRegistry, authStorage, credits);
46
+ }
24
47
  const model = await resolveModel(config, modelRegistry);
25
48
  const settingsManager = SettingsManager.create(cwd, agentDir);
26
49
  const loader = new DefaultResourceLoader({
27
50
  cwd,
28
51
  agentDir,
29
52
  settingsManager,
30
- ...(config.systemPrompt
31
- ? { systemPromptOverride: () => config.systemPrompt }
32
- : {}),
33
53
  });
34
54
  await loader.reload();
35
55
  const sessionManager = persisted.sessionFile
@@ -41,10 +61,6 @@ export const getOrCreatePiSession = async (args) => {
41
61
  authStorage,
42
62
  modelRegistry,
43
63
  model,
44
- thinkingLevel: config.thinkingLevel,
45
- tools: parseToolList(config.tools),
46
- excludeTools: parseToolList(config.excludeTools),
47
- noTools: config.noTools,
48
64
  resourceLoader: loader,
49
65
  sessionManager,
50
66
  settingsManager,
@@ -66,3 +82,10 @@ export const disposePiSession = (sessionKey) => {
66
82
  cached.session.dispose();
67
83
  sessionCache.delete(sessionKey);
68
84
  };
85
+ export const disposePiSessionsForThread = (state) => {
86
+ const prefix = state.threadId ? `${state.channelId}:${state.threadId}:` : `${state.channelId}:`;
87
+ for (const key of sessionCache.keys()) {
88
+ if (key.startsWith(prefix))
89
+ disposePiSession(key);
90
+ }
91
+ };
package/dist/stream.js CHANGED
@@ -1,8 +1,8 @@
1
- import { createStreamState, formatPiEvent } from './format.js';
2
- /** Bridge Pi session events into an async generator of output chunks. */
3
- export async function* streamPiPrompt(session, prompt, options) {
4
- const state = createStreamState();
5
- const pending = [];
1
+ const isAssistantMessage = (message) => message.role === 'assistant';
2
+ /** Run a Pi prompt and return the final assistant text. */
3
+ export async function runPiPrompt(session, prompt, options) {
4
+ let assistantText = '';
5
+ let result = '';
6
6
  let wake;
7
7
  let finished = false;
8
8
  let error;
@@ -10,17 +10,26 @@ export async function* streamPiPrompt(session, prompt, options) {
10
10
  wake?.();
11
11
  wake = undefined;
12
12
  };
13
- const unsubscribe = session.subscribe((event) => {
14
- const result = formatPiEvent(event, state);
15
- if (result) {
16
- pending.push(result);
17
- wakeUp();
13
+ const onEvent = (event) => {
14
+ if (event.type === 'message_update') {
15
+ const assistantEvent = event.assistantMessageEvent;
16
+ if (assistantEvent.type === 'text_delta')
17
+ assistantText += assistantEvent.delta;
18
+ return;
18
19
  }
19
20
  if (event.type === 'agent_end') {
21
+ const lastAssistant = [...event.messages].reverse().find(isAssistantMessage);
22
+ if (lastAssistant?.stopReason === 'error' && lastAssistant.errorMessage?.trim()) {
23
+ result = `Pi error: ${lastAssistant.errorMessage.trim()}`;
24
+ }
25
+ else {
26
+ result = assistantText;
27
+ }
20
28
  finished = true;
21
29
  wakeUp();
22
30
  }
23
- });
31
+ };
32
+ const unsubscribe = session.subscribe(onEvent);
24
33
  const run = (async () => {
25
34
  try {
26
35
  if (options?.streaming && session.isStreaming) {
@@ -39,18 +48,19 @@ export async function* streamPiPrompt(session, prompt, options) {
39
48
  }
40
49
  })();
41
50
  try {
42
- while (!finished || pending.length > 0) {
43
- if (pending.length === 0) {
44
- await new Promise((resolve) => {
45
- wake = resolve;
46
- });
47
- continue;
48
- }
49
- yield pending.shift();
51
+ while (!finished) {
52
+ await new Promise((resolve) => {
53
+ if (finished) {
54
+ resolve();
55
+ return;
56
+ }
57
+ wake = resolve;
58
+ });
50
59
  }
51
60
  await run;
52
61
  if (error)
53
62
  throw error;
63
+ return result.trim();
54
64
  }
55
65
  finally {
56
66
  unsubscribe();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meetopenbot/pi",
3
- "version": "0.0.3",
3
+ "version": "0.1.1",
4
4
  "description": "OpenBot agent plugin powered by the Pi coding agent SDK.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -21,7 +21,7 @@
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.2.0"
24
+ "@meetopenbot/plugin-sdk": "^0.3.0"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@types/node": "^25.9.2",
@@ -31,7 +31,7 @@
31
31
  "scripts": {
32
32
  "build": "tsc -p tsconfig.json && node ../../scripts/write-plugin-declaration.mjs",
33
33
  "typecheck": "tsc -p tsconfig.json --noEmit",
34
- "test": "node --experimental-strip-types --test src/diff.test.ts",
34
+ "test": "node --experimental-strip-types --test src/*.test.ts",
35
35
  "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput"
36
36
  }
37
37
  }
package/dist/diff.js DELETED
@@ -1 +0,0 @@
1
- export { buildDiffWidget as runDiffWidget, diffFileFromMutationTool as diffFileFromPiTool, } from '@meetopenbot/plugin-sdk';
package/dist/format.js DELETED
@@ -1,148 +0,0 @@
1
- const isAssistantMessage = (message) => message.role === 'assistant';
2
- export const createStreamState = () => ({
3
- assistantText: '',
4
- });
5
- const strArg = (args, ...keys) => {
6
- if (!args || typeof args !== 'object')
7
- return undefined;
8
- const record = args;
9
- for (const key of keys) {
10
- const value = record[key];
11
- if (typeof value === 'string' && value.trim())
12
- return value.trim();
13
- if (typeof value === 'number' && Number.isFinite(value))
14
- return String(value);
15
- }
16
- return undefined;
17
- };
18
- const truncate = (text, max = 80) => text.length > max ? `${text.slice(0, max - 3)}…` : text;
19
- const quoteDetail = (detail) => detail.includes('`') ? `"${detail}"` : `\`${detail}\``;
20
- const formatToolDetail = (toolName, args) => {
21
- switch (toolName) {
22
- case 'bash': {
23
- const command = strArg(args, 'command');
24
- return command ? truncate(command.replace(/\s+/g, ' ')) : undefined;
25
- }
26
- case 'read':
27
- case 'edit':
28
- case 'write': {
29
- const path = strArg(args, 'path', 'file_path');
30
- if (!path)
31
- return undefined;
32
- let detail = truncate(path, 120);
33
- if (toolName === 'read') {
34
- const offset = strArg(args, 'offset');
35
- const limit = strArg(args, 'limit');
36
- const parts = [offset && `offset=${offset}`, limit && `limit=${limit}`].filter(Boolean);
37
- if (parts.length)
38
- detail += ` (${parts.join(', ')})`;
39
- }
40
- return detail;
41
- }
42
- case 'grep': {
43
- const pattern = strArg(args, 'pattern');
44
- const path = strArg(args, 'path') ?? '.';
45
- if (!pattern)
46
- return undefined;
47
- return `${truncate(pattern)} in ${truncate(path, 60)}`;
48
- }
49
- case 'find': {
50
- const pattern = strArg(args, 'pattern');
51
- const path = strArg(args, 'path') ?? '.';
52
- if (!pattern)
53
- return undefined;
54
- return `${truncate(pattern)} in ${truncate(path, 60)}`;
55
- }
56
- case 'ls': {
57
- const path = strArg(args, 'path') ?? '.';
58
- return truncate(path, 120);
59
- }
60
- default:
61
- return undefined;
62
- }
63
- };
64
- const formatToolStart = (toolName, args) => {
65
- const detail = formatToolDetail(toolName, args);
66
- if (detail)
67
- return `Running **${toolName}** (${quoteDetail(detail)})…`;
68
- return `Running **${toolName}**…`;
69
- };
70
- /** Map Pi session events to user-visible output chunks. */
71
- export const formatPiEvent = (event, state) => {
72
- switch (event.type) {
73
- case 'message_update': {
74
- const assistantEvent = event.assistantMessageEvent;
75
- if (assistantEvent.type !== 'text_delta')
76
- return undefined;
77
- state.assistantText += assistantEvent.delta;
78
- return undefined;
79
- }
80
- case 'tool_execution_start': {
81
- const statusLine = formatToolStart(event.toolName, event.args);
82
- return {
83
- type: 'tool_start',
84
- toolCallId: event.toolCallId,
85
- toolName: event.toolName,
86
- args: event.args,
87
- statusLine,
88
- };
89
- }
90
- case 'tool_execution_end': {
91
- let statusLine;
92
- if (event.isError) {
93
- statusLine = `Tool **${event.toolName}** failed.`;
94
- }
95
- return {
96
- type: 'tool_end',
97
- toolCallId: event.toolCallId,
98
- toolName: event.toolName,
99
- result: event.result,
100
- isError: event.isError,
101
- statusLine,
102
- };
103
- }
104
- case 'auto_retry_start': {
105
- const statusLine = `Retrying (${event.attempt}/${event.maxAttempts})…`;
106
- return { type: 'status', statusLine };
107
- }
108
- case 'compaction_start': {
109
- const statusLine = 'Compacting conversation context…';
110
- return { type: 'status', statusLine };
111
- }
112
- case 'agent_end': {
113
- const lastAssistant = [...event.messages].reverse().find(isAssistantMessage);
114
- if (lastAssistant?.stopReason === 'error' && lastAssistant.errorMessage?.trim()) {
115
- return { type: 'text', content: `**Pi error:** ${lastAssistant.errorMessage.trim()}` };
116
- }
117
- if (state.assistantText.trim()) {
118
- return { type: 'text', content: state.assistantText };
119
- }
120
- return undefined;
121
- }
122
- default:
123
- return undefined;
124
- }
125
- };
126
- export const formatPiError = (error) => {
127
- if (error instanceof Error)
128
- return error.message;
129
- return String(error);
130
- };
131
- export const formatToolResult = (result) => {
132
- if (!result)
133
- return '';
134
- if (typeof result === 'string')
135
- return result;
136
- if (result && typeof result === 'object' && Array.isArray(result.content)) {
137
- return result.content
138
- .map((c) => {
139
- if (c.type === 'text')
140
- return c.text;
141
- if (c.type === 'image')
142
- return `[Image: ${c.source?.data?.slice(0, 20)}...]`;
143
- return `[${c.type}]`;
144
- })
145
- .join('\n');
146
- }
147
- return JSON.stringify(result, null, 2);
148
- };