@composio/experimental 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 ADDED
@@ -0,0 +1,190 @@
1
+ # `@composio/experimental`
2
+
3
+ Experimental Composio integrations and helpers.
4
+
5
+ This package currently includes a Pi provider for [`@earendil-works/pi-coding-agent`](https://www.npmjs.com/package/@earendil-works/pi-coding-agent). It lets Composio tools be passed to Pi SDK sessions as `customTools` and includes a dynamic Tool Router session toolset modeled after the Slack bot integration in `~/composio/slack-bot`.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add @composio/core @composio/experimental @earendil-works/pi-coding-agent
11
+ ```
12
+
13
+ ## Static tool wrapping
14
+
15
+ Use this when you already know the exact Composio tools to expose to Pi.
16
+
17
+ ```ts
18
+ import { Composio } from '@composio/core';
19
+ import { PiProvider } from '@composio/experimental';
20
+ import { createAgentSession, SessionManager } from '@earendil-works/pi-coding-agent';
21
+
22
+ const provider = new PiProvider();
23
+ const composio = new Composio({
24
+ apiKey: process.env.COMPOSIO_API_KEY,
25
+ provider,
26
+ });
27
+
28
+ const tools = await composio.tools.get('default', {
29
+ tools: ['GITHUB_CREATE_ISSUE'],
30
+ });
31
+
32
+ const { session } = await createAgentSession({
33
+ cwd: process.cwd(),
34
+ sessionManager: SessionManager.inMemory(process.cwd()),
35
+ customTools: tools,
36
+ tools: ['read', 'bash', ...tools.map(tool => tool.name)],
37
+ });
38
+
39
+ await session.prompt('Create a GitHub issue for the failing test.');
40
+ ```
41
+
42
+ ## Dynamic session helpers
43
+
44
+ Use this when Pi should search and execute tools dynamically inside one Tool Router session. Prefer the capability form so your app owns connection management and uses one Pi-style `hooks` object for interception/result transforms.
45
+
46
+ ```ts
47
+ import { Composio } from '@composio/core';
48
+ import { PiProvider, createPiComposioSystemPrompt } from '@composio/experimental';
49
+ import {
50
+ createAgentSession,
51
+ DefaultResourceLoader,
52
+ getAgentDir,
53
+ SessionManager,
54
+ } from '@earendil-works/pi-coding-agent';
55
+
56
+ const provider = new PiProvider();
57
+ const composio = new Composio({ apiKey: process.env.COMPOSIO_API_KEY });
58
+
59
+ const composioSession = await composio.sessions.create('slack:T123:U456', {
60
+ toolkits: ['github', 'gmail'],
61
+ manageConnections: true,
62
+ workbench: { enable: true },
63
+ });
64
+
65
+ const composioTools = provider.createSessionTools({
66
+ sessionId: composioSession.sessionId,
67
+ search: composioSession.search.bind(composioSession),
68
+ execute: composioSession.execute.bind(composioSession),
69
+ callbackUrl: 'https://your-app.example.com/auth/callback',
70
+ includeWorkbenchTools: true,
71
+ connections: {
72
+ getToolkitStates: toolkits => composioSession.toolkits({ toolkits }),
73
+ authorizeToolkit: (toolkit, options) => composioSession.authorize(toolkit, options),
74
+ isConnected: state => state.connection?.isActive === true,
75
+ },
76
+ hooks: {
77
+ search: (ctx, next) => {
78
+ ctx.request.toolkits = ctx.request.toolkits?.map(toolkit =>
79
+ toolkit === 'slack' ? 'slackbot' : toolkit
80
+ );
81
+ return next();
82
+ },
83
+ execute: (ctx, next) => {
84
+ if (ctx.request.toolSlug === 'COMPOSIO_MANAGE_CONNECTIONS') {
85
+ return ctx.deny('Meta tools are blocked.');
86
+ }
87
+ return next();
88
+ },
89
+ remoteWorkbench: async (ctx, next) => {
90
+ const result = await next();
91
+ await auditWorkbenchRun({ code: ctx.request.code_to_execute, result });
92
+ return result;
93
+ },
94
+ remoteBash: async (ctx, next) => {
95
+ if (ctx.request.command.includes('rm -rf')) {
96
+ return ctx.deny('Destructive bash commands are blocked.');
97
+ }
98
+ return next();
99
+ },
100
+ onAuthLink: async (ctx, next) => {
101
+ // DM the user, store the continuation, or redact public output.
102
+ await sendConnectionLinkToUser({ url: ctx.url, toolkit: ctx.toolkit });
103
+ return { message: 'Connection link sent out-of-band.' };
104
+ // To also send the original result/link to the model, use: return next();
105
+ },
106
+ },
107
+ transformResult: async ({ value }) => value,
108
+ });
109
+
110
+ const loader = new DefaultResourceLoader({
111
+ cwd: process.cwd(),
112
+ agentDir: getAgentDir(),
113
+ systemPromptOverride: () =>
114
+ createPiComposioSystemPrompt(composioSession.sessionId, { includeWorkbenchTools: true }),
115
+ });
116
+ await loader.reload();
117
+
118
+ const { session } = await createAgentSession({
119
+ cwd: process.cwd(),
120
+ resourceLoader: loader,
121
+ sessionManager: SessionManager.inMemory(process.cwd()),
122
+ customTools: composioTools,
123
+ tools: [
124
+ 'read',
125
+ 'bash',
126
+ 'composio_search_tools',
127
+ 'composio_manage_connections',
128
+ 'composio_execute_tool',
129
+ 'composio_remote_workbench',
130
+ 'composio_remote_bash',
131
+ ],
132
+ });
133
+
134
+ await session.prompt('Find my recent GitHub issues and summarize the blockers.');
135
+ ```
136
+
137
+ `composio_manage_connections` uses your `connections.getToolkitStates()` and `connections.authorizeToolkit()` handlers. It does **not** call `session.execute('COMPOSIO_MANAGE_CONNECTIONS', ...)` internally.
138
+
139
+ The dynamic helpers are:
140
+
141
+ - `composio_search_tools` — search Tool Router for exact tool slugs and schemas.
142
+ - `composio_manage_connections` — check/initiate user app connections.
143
+ - `composio_execute_tool` — execute exact Composio tool slugs in the session.
144
+ - `composio_remote_workbench` — execute Python in the Composio remote workbench for large outputs, remote files, and session-authenticated scripting.
145
+ - `composio_remote_bash` — run short bash commands in the Composio remote workbench filesystem.
146
+
147
+ Workbench helpers are opt-in because the Tool Router session must be created with workbench enabled.
148
+
149
+ ## Hooks
150
+
151
+ `hooks` follows Pi's extension-event style with middleware semantics. Each hook receives a mutable `ctx.request`, `ctx.deny('reason')`, and a typed `next()` function. Calling `await next()` runs the default Composio behavior and returns the result before anything is sent back to the model. Return that result to pass it through, return a replacement value to control what the model sees, or skip `next()` entirely to divert/deny the operation.
152
+
153
+ Available hooks:
154
+
155
+ - `search(ctx, next)` — rewrite query/toolkit filters, log search results, or return custom search output.
156
+ - `manageConnections(ctx, next)` — rewrite requested toolkits, force reauth, log connection state, or return custom connection output.
157
+ - `execute(ctx, next)` — block/rewrite tools, route to another session/execute handler, call `ctx.manageConnections(...)`, log outputs, or return a file/workbench reference instead of inline data.
158
+ - `remoteWorkbench(ctx, next)` — rewrite Python code/session metadata, audit workbench runs, or replace large outputs with file/workbench references. Calls through the generic `execute` hook when `next()` is used.
159
+ - `remoteBash(ctx, next)` — rewrite/block shell commands, enforce safety policy, audit filesystem access, or replace output. Calls through the generic `execute` hook when `next()` is used.
160
+ - `onAuthLink(ctx, next)` — send/redact/resume auth links out-of-band. `return next()` keeps the original model-visible result; returning another value replaces it.
161
+
162
+ ## Auth link handling
163
+
164
+ Embedded agents often need to keep Composio connection URLs out of the public transcript. Use `hooks.onAuthLink()` to choose whether the model sees the original result or a redacted replacement:
165
+
166
+ ```ts
167
+ const tools = provider.createSessionTools({
168
+ search: composioSession.search.bind(composioSession),
169
+ execute: composioSession.execute.bind(composioSession),
170
+ connections: {
171
+ getToolkitStates: toolkits => composioSession.toolkits({ toolkits }),
172
+ authorizeToolkit: (toolkit, options) => composioSession.authorize(toolkit, options),
173
+ },
174
+ hooks: {
175
+ onAuthLink: async (ctx, next) => {
176
+ await sendConnectionLinkToUser({ url: ctx.url, toolkit: ctx.toolkit });
177
+
178
+ if (shouldAlsoShowLinkToModel(ctx)) {
179
+ return next();
180
+ }
181
+
182
+ return { message: 'Connection link sent out-of-band.' };
183
+ },
184
+ },
185
+ });
186
+ ```
187
+
188
+ ## Status
189
+
190
+ Experimental. The dynamic helper names and session helper API may change.
@@ -0,0 +1,266 @@
1
+ import { BaseAgenticProvider, ExecuteToolFn, McpServerGetResponse, McpUrlResponse, Tool } from "@composio/core";
2
+ import { ToolDefinition } from "@earendil-works/pi-coding-agent";
3
+
4
+ //#region src/pi/types.d.ts
5
+ type PiToolDetails = {
6
+ slug?: string;
7
+ result?: unknown;
8
+ error?: string | null;
9
+ authLinks?: string[];
10
+ denied?: boolean;
11
+ };
12
+ type PiTool = ToolDefinition;
13
+ type PiToolCollection = PiTool[];
14
+ type PiToolResultFormatter = (result: unknown) => string;
15
+ type PiDeniedResult = {
16
+ successful: false;
17
+ error: string;
18
+ data: null;
19
+ denied: true;
20
+ };
21
+ interface PiHookControls {
22
+ /** Return from a hook to explicitly deny/divert the operation with a model-visible error. */
23
+ deny(error: string): PiDeniedResult;
24
+ }
25
+ type MaybePromise<T> = T | Promise<T>;
26
+ type PiHookNext<TResult> = () => Promise<TResult>;
27
+ interface PiProviderOptions {
28
+ /** Prefix shown in Pi's TUI labels. Defaults to `Composio`. */
29
+ labelPrefix?: string;
30
+ /** Per-tool execution mode. Defaults to Pi's runtime default. */
31
+ executionMode?: PiTool['executionMode'];
32
+ /** Convert Composio results to the text sent back to the model. */
33
+ formatResult?: PiToolResultFormatter;
34
+ /** Return thrown errors as structured JSON instead of rethrowing to Pi. Defaults to true. */
35
+ catchErrors?: boolean;
36
+ }
37
+ declare const DEFAULT_SESSION_TOOL_NAMES: {
38
+ readonly search: "composio_search_tools";
39
+ readonly manageConnections: "composio_manage_connections";
40
+ readonly execute: "composio_execute_tool";
41
+ readonly remoteWorkbench: "composio_remote_workbench";
42
+ readonly remoteBash: "composio_remote_bash";
43
+ };
44
+ type PiSessionToolName = keyof typeof DEFAULT_SESSION_TOOL_NAMES;
45
+ interface PiBaseToolContext {
46
+ /** Tool call id passed by Pi. */
47
+ toolCallId: string;
48
+ /** The Pi helper tool that is currently running. */
49
+ sourceTool: string;
50
+ /** Optional Composio Tool Router session id for prompts/logging/default workbench session ids. */
51
+ sessionId?: string;
52
+ /** Original helper request sent by the model. */
53
+ originalRequest: unknown;
54
+ }
55
+ interface PiSearchContext extends PiBaseToolContext {
56
+ query: string;
57
+ requestedToolkits?: string[];
58
+ }
59
+ interface PiConnectionManagementContext extends PiBaseToolContext {
60
+ requestedToolkits: string[];
61
+ callbackUrl?: string;
62
+ reinitiateAll: boolean;
63
+ }
64
+ interface PiExecuteContext extends PiBaseToolContext {
65
+ toolSlug: string;
66
+ toolkit?: string;
67
+ args: Record<string, unknown>;
68
+ account?: string;
69
+ }
70
+ interface PiAuthLinkContext<TResult = unknown> extends PiHookControls {
71
+ url: string;
72
+ toolkit?: string;
73
+ sourceTool: string;
74
+ originalRequest: unknown;
75
+ result: TResult;
76
+ sessionId?: string;
77
+ }
78
+ interface PiSessionToolOptions extends PiProviderOptions {
79
+ /** Callback URL passed to `authorizeToolkit()` when connection management initiates auth. */
80
+ callbackUrl?: string;
81
+ /** Include first-class wrappers for COMPOSIO_REMOTE_WORKBENCH and COMPOSIO_REMOTE_BASH_TOOL. Defaults to false. */
82
+ includeWorkbenchTools?: boolean;
83
+ /** Override the default helper tool names. */
84
+ names?: Partial<typeof DEFAULT_SESSION_TOOL_NAMES>;
85
+ /** Called before a result is returned to Pi; useful for redacting or routing auth links. */
86
+ transformResult?: (params: {
87
+ tool: PiSessionToolName;
88
+ requestedToolkits?: string[];
89
+ value: unknown;
90
+ context?: PiBaseToolContext;
91
+ }) => unknown | Promise<unknown>;
92
+ }
93
+ interface PiComposioSessionLike<TSearchResult = unknown, TExecuteResult = unknown, TToolkitStates = unknown, TAuthorizeResult = unknown> {
94
+ sessionId?: string;
95
+ search(params: {
96
+ query: string;
97
+ toolkits?: string[];
98
+ }): Promise<TSearchResult>;
99
+ execute(toolSlug: string, args?: Record<string, unknown>, options?: {
100
+ account?: string;
101
+ }): Promise<TExecuteResult>;
102
+ /** Native Tool Router connection-state API. Preferred over executing a meta tool. */
103
+ toolkits?(options?: {
104
+ toolkits?: string[];
105
+ isConnected?: boolean;
106
+ limit?: number;
107
+ cursor?: string;
108
+ }): Promise<TToolkitStates>;
109
+ authorize?(toolkit: string, options?: {
110
+ callbackUrl?: string;
111
+ alias?: string;
112
+ experimental?: unknown;
113
+ }): Promise<TAuthorizeResult>;
114
+ }
115
+ interface PiExecutableSessionLike<TExecuteResult = unknown> {
116
+ sessionId?: string;
117
+ execute(toolSlug: string, args?: Record<string, unknown>, options?: {
118
+ account?: string;
119
+ }): Promise<TExecuteResult>;
120
+ }
121
+ type PiSearchHandler<TSearchResult = unknown> = (params: {
122
+ query: string;
123
+ toolkits?: string[];
124
+ }, context: PiSearchContext) => MaybePromise<TSearchResult>;
125
+ type PiExecuteHandler<TExecuteResult = unknown> = (toolSlug: string, args: Record<string, unknown>, options: {
126
+ account?: string;
127
+ } | undefined, context: PiExecuteContext) => MaybePromise<TExecuteResult>;
128
+ interface PiAuthorizeToolkitOptions {
129
+ callbackUrl?: string;
130
+ alias?: string;
131
+ experimental?: unknown;
132
+ reinitiate?: boolean;
133
+ }
134
+ interface PiConnectionHandlers<TState = unknown, TAuthorizeResult = unknown, TToolkitStates = unknown> {
135
+ /** Return connection states for the requested toolkits, e.g. from `session.toolkits({ toolkits })`. */
136
+ getToolkitStates?: (toolkits: string[], context: PiConnectionManagementContext) => MaybePromise<TToolkitStates>;
137
+ /** Start auth for one toolkit, e.g. via `session.authorize(toolkit, { callbackUrl })`. */
138
+ authorizeToolkit?: (toolkit: string, options: PiAuthorizeToolkitOptions, context: PiConnectionManagementContext) => Promise<TAuthorizeResult> | TAuthorizeResult;
139
+ /** Interpret one state returned by `getToolkitStates()`. Defaults handle common Tool Router shapes. */
140
+ isConnected?: (state: TState, context: {
141
+ toolkit: string;
142
+ request: PiConnectionManagementContext;
143
+ }) => boolean;
144
+ }
145
+ interface PiSearchHookContext extends PiHookControls {
146
+ request: {
147
+ query: string;
148
+ toolkits?: string[];
149
+ };
150
+ context: PiSearchContext;
151
+ }
152
+ interface PiManageConnectionsHookContext extends PiHookControls {
153
+ request: {
154
+ toolkits: string[];
155
+ reinitiateAll: boolean;
156
+ };
157
+ context: PiConnectionManagementContext;
158
+ }
159
+ interface PiExecuteHookContext<TExecuteResult = unknown> extends PiHookControls {
160
+ request: {
161
+ toolSlug: string;
162
+ args: Record<string, unknown>;
163
+ account?: string;
164
+ session?: PiExecutableSessionLike<TExecuteResult>;
165
+ execute?: PiExecuteHandler<TExecuteResult>;
166
+ };
167
+ context: PiExecuteContext;
168
+ manageConnections: (toolkits: string[], options?: {
169
+ reinitiateAll?: boolean;
170
+ }) => Promise<unknown>;
171
+ }
172
+ interface PiRemoteWorkbenchRequest extends Record<string, unknown> {
173
+ code_to_execute: string;
174
+ timeout?: number;
175
+ thought?: string;
176
+ file_path?: string;
177
+ disabled_tools?: string[];
178
+ session_id?: string;
179
+ }
180
+ interface PiRemoteBashRequest extends Record<string, unknown> {
181
+ command: string;
182
+ session_id?: string;
183
+ }
184
+ interface PiRemoteWorkbenchHookContext extends PiHookControls {
185
+ request: PiRemoteWorkbenchRequest;
186
+ context: PiExecuteContext;
187
+ }
188
+ interface PiRemoteBashHookContext extends PiHookControls {
189
+ request: PiRemoteBashRequest;
190
+ context: PiExecuteContext;
191
+ }
192
+ interface PiSessionHooks<TSearchResult = unknown, TExecuteResult = unknown, TState = unknown, TAuthorizeResult = unknown> {
193
+ /** Middleware around search. Mutate `ctx.request`, call `next()` to run search, or return a replacement result. */
194
+ search?: (ctx: PiSearchHookContext, next: PiHookNext<TSearchResult>) => MaybePromise<unknown>;
195
+ /** Middleware around connection management. Mutate `ctx.request`, call `next()` to check/connect, or return a replacement result. */
196
+ manageConnections?: (ctx: PiManageConnectionsHookContext, next: PiHookNext<PiConnectionManagementResult<TState, TAuthorizeResult>>) => MaybePromise<unknown>;
197
+ /** Middleware around tool execution. Mutate `ctx.request`, call `next()` to execute, or return a replacement result. */
198
+ execute?: (ctx: PiExecuteHookContext<TExecuteResult>, next: PiHookNext<TExecuteResult>) => MaybePromise<unknown>;
199
+ /** Middleware around the remote Python workbench helper. Runs outside, then through, the generic `execute` hook when `next()` is called. */
200
+ remoteWorkbench?: (ctx: PiRemoteWorkbenchHookContext, next: PiHookNext<TExecuteResult>) => MaybePromise<unknown>;
201
+ /** Middleware around the remote bash helper. Runs outside, then through, the generic `execute` hook when `next()` is called. */
202
+ remoteBash?: (ctx: PiRemoteBashHookContext, next: PiHookNext<TExecuteResult>) => MaybePromise<unknown>;
203
+ /** Middleware for auth links found in any result. Call `next()` to keep the current model-visible result, or return a replacement. */
204
+ onAuthLink?: (ctx: PiAuthLinkContext<TSearchResult | TExecuteResult | TAuthorizeResult>, next: PiHookNext<TSearchResult | TExecuteResult | TAuthorizeResult>) => MaybePromise<unknown>;
205
+ }
206
+ interface PiSessionToolCapabilities<TSearchResult = unknown, TExecuteResult = unknown, TState = unknown, TAuthorizeResult = unknown, TToolkitStates = unknown> extends PiSessionToolOptions {
207
+ /** Optional session id for prompt context and default workbench session ids. */
208
+ sessionId?: string;
209
+ search: PiSearchHandler<TSearchResult>;
210
+ execute: PiExecuteHandler<TExecuteResult>;
211
+ connections?: PiConnectionHandlers<TState, TAuthorizeResult, TToolkitStates>;
212
+ hooks?: PiSessionHooks<TSearchResult, TExecuteResult, TState, TAuthorizeResult>;
213
+ }
214
+ type PiConnectionToolkitResult<TState = unknown, TAuthorizeResult = unknown> = {
215
+ toolkit: string;
216
+ connected: boolean;
217
+ status: 'connected' | 'auth_initiated' | 'missing_authorize_handler';
218
+ state?: TState;
219
+ authorization?: TAuthorizeResult;
220
+ authLinks?: string[];
221
+ };
222
+ type PiConnectionManagementResult<TState = unknown, TAuthorizeResult = unknown> = {
223
+ successful: true;
224
+ data: {
225
+ message: string;
226
+ results: Record<string, PiConnectionToolkitResult<TState, TAuthorizeResult>>;
227
+ };
228
+ error: null;
229
+ };
230
+ //#endregion
231
+ //#region src/pi/auth-links.d.ts
232
+ declare const extractComposioConnectLinks: (value: unknown) => string[];
233
+ //#endregion
234
+ //#region src/pi/provider.d.ts
235
+ /**
236
+ * Provider for integrating Composio tools with Pi SDK custom tools.
237
+ */
238
+ declare class PiProvider extends BaseAgenticProvider<PiToolCollection, PiTool, McpServerGetResponse> {
239
+ private readonly options;
240
+ readonly name = "pi";
241
+ constructor(options?: PiProviderOptions);
242
+ /**
243
+ * Wrap a concrete Composio tool as a Pi custom tool definition.
244
+ */
245
+ wrapTool(composioTool: Tool, executeTool: ExecuteToolFn): PiTool;
246
+ /**
247
+ * Wrap multiple concrete Composio tools as Pi custom tools.
248
+ */
249
+ wrapTools(tools: Tool[], executeTool: ExecuteToolFn): PiToolCollection;
250
+ createSessionTools<TSearchResult = unknown, TExecuteResult = unknown, TState = unknown, TAuthorizeResult = unknown, TToolkitStates = unknown>(capabilities: PiSessionToolCapabilities<TSearchResult, TExecuteResult, TState, TAuthorizeResult, TToolkitStates>): PiToolCollection;
251
+ createSessionTools<TSearchResult = unknown, TExecuteResult = unknown, TToolkitStates = unknown, TAuthorizeResult = unknown>(session: PiComposioSessionLike<TSearchResult, TExecuteResult, TToolkitStates, TAuthorizeResult>, options?: PiSessionToolOptions): PiToolCollection;
252
+ /**
253
+ * Transform MCP URL responses into Pi-compatible standard URL entries.
254
+ */
255
+ wrapMcpServerResponse(data: McpUrlResponse): McpServerGetResponse;
256
+ }
257
+ //#endregion
258
+ //#region src/pi/prompt.d.ts
259
+ declare const createPiComposioSystemPrompt: (sessionId?: string, options?: {
260
+ includeWorkbenchTools?: boolean;
261
+ }) => string;
262
+ //#endregion
263
+ //#region src/pi/results.d.ts
264
+ declare const denyPiToolCall: (error: string) => PiDeniedResult;
265
+ //#endregion
266
+ export { type MaybePromise, DEFAULT_SESSION_TOOL_NAMES as PI_COMPOSIO_SESSION_TOOL_NAMES, type PiAuthLinkContext, type PiAuthorizeToolkitOptions, type PiBaseToolContext, type PiComposioSessionLike, type PiConnectionHandlers, type PiConnectionManagementContext, type PiConnectionManagementResult, type PiConnectionToolkitResult, type PiDeniedResult, type PiExecutableSessionLike, type PiExecuteContext, type PiExecuteHandler, type PiExecuteHookContext, type PiHookControls, type PiHookNext, type PiManageConnectionsHookContext, PiProvider, type PiProviderOptions, type PiRemoteBashHookContext, type PiRemoteBashRequest, type PiRemoteWorkbenchHookContext, type PiRemoteWorkbenchRequest, type PiSearchContext, type PiSearchHandler, type PiSearchHookContext, type PiSessionHooks, type PiSessionToolCapabilities, type PiSessionToolName, type PiSessionToolOptions, type PiTool, type PiToolCollection, type PiToolDetails, type PiToolResultFormatter, createPiComposioSystemPrompt, denyPiToolCall, extractComposioConnectLinks };
package/dist/index.mjs ADDED
@@ -0,0 +1,694 @@
1
+ import { BaseAgenticProvider, normalizeToolArguments } from "@composio/core";
2
+ import { defineTool } from "@earendil-works/pi-coding-agent";
3
+ import { Type } from "typebox";
4
+
5
+ //#region src/pi/hooks.ts
6
+ const runHook = async (hook, context, getDefaultResult) => {
7
+ if (!hook) return getDefaultResult();
8
+ const state = {};
9
+ const next = async () => {
10
+ state.nextResult ??= Promise.resolve(getDefaultResult());
11
+ return state.nextResult;
12
+ };
13
+ const hookValue = await hook(context, next);
14
+ if (hookValue !== void 0) return hookValue;
15
+ return state.nextResult ?? next();
16
+ };
17
+
18
+ //#endregion
19
+ //#region src/pi/results.ts
20
+ const denyPiToolCall = (error) => ({
21
+ successful: false,
22
+ error,
23
+ data: null,
24
+ denied: true
25
+ });
26
+ const hookControls = { deny: denyPiToolCall };
27
+ const defaultFormatResult = (result) => JSON.stringify(result, null, 2);
28
+ const stringifyError = (error) => error instanceof Error ? error.message : String(error);
29
+ const isPiDeniedResult = (value) => !!value && typeof value === "object" && value.denied === true && value.successful === false;
30
+ const toPiResult = (value, formatter, details = {}) => ({
31
+ content: [{
32
+ type: "text",
33
+ text: formatter(value)
34
+ }],
35
+ details: {
36
+ ...details,
37
+ denied: details.denied ?? (isPiDeniedResult(value) ? true : void 0),
38
+ result: value
39
+ }
40
+ });
41
+ const toPiErrorResult = (error, formatter, details = {}) => {
42
+ const message = stringifyError(error);
43
+ const value = {
44
+ successful: false,
45
+ error: message,
46
+ data: null
47
+ };
48
+ return {
49
+ content: [{
50
+ type: "text",
51
+ text: formatter(value)
52
+ }],
53
+ details: {
54
+ ...details,
55
+ error: message,
56
+ result: value
57
+ }
58
+ };
59
+ };
60
+
61
+ //#endregion
62
+ //#region src/pi/auth-links.ts
63
+ const extractComposioConnectLinks = (value) => {
64
+ const text = stringifyUnknown(value);
65
+ const connectLinks = text.match(/https:\/\/connect\.composio\.dev\/[^\s<>)"']+/gi) ?? [];
66
+ const genericLinks = text.match(/https:\/\/[^\s<>)"']*composio[^\s<>)"']*\/link\/[^\s<>)"']+/gi) ?? [];
67
+ return [...new Set([...connectLinks, ...genericLinks].map((url) => url.replace(/[.,;:!?]+$/g, "")))];
68
+ };
69
+ const stringifyUnknown = (value) => {
70
+ if (typeof value === "string") return value;
71
+ try {
72
+ return JSON.stringify(value);
73
+ } catch {
74
+ return String(value);
75
+ }
76
+ };
77
+ const applyAuthLinkHandlers = async (capabilities, value, context) => {
78
+ const links = extractComposioConnectLinks(value);
79
+ const run = async (index, currentValue) => {
80
+ const url = links[index];
81
+ if (!url) return currentValue;
82
+ return run(index + 1, await runHook(capabilities.hooks?.onAuthLink, {
83
+ ...context,
84
+ ...hookControls,
85
+ url,
86
+ result: currentValue
87
+ }, async () => currentValue));
88
+ };
89
+ return {
90
+ value: await run(0, value),
91
+ authLinks: links
92
+ };
93
+ };
94
+
95
+ //#endregion
96
+ //#region src/pi/schemas.ts
97
+ const EmptyObjectSchema = Type.Object({});
98
+ const ToolkitsSchema = Type.Optional(Type.Array(Type.String({ description: "Optional toolkit slug filter, e.g. github, gmail." })));
99
+ const objectInputSchema = (schema) => {
100
+ const candidate = schema && typeof schema === "object" ? { ...schema } : { ...EmptyObjectSchema };
101
+ if (!candidate.type) candidate.type = "object";
102
+ if (!candidate.properties) candidate.properties = {};
103
+ if (candidate.additionalProperties === void 0) candidate.additionalProperties = true;
104
+ return Type.Unsafe(candidate);
105
+ };
106
+ const optionalRecordSchema = (description) => Type.Optional(Type.Record(Type.String(), Type.Any(), { description }));
107
+
108
+ //#endregion
109
+ //#region src/pi/utils.ts
110
+ const normalizeToolkits = (value) => {
111
+ const toolkits = Array.isArray(value) ? value.filter((item) => typeof item === "string" && item.trim().length > 0) : typeof value === "string" && value.trim().length > 0 ? [value] : [];
112
+ const unique = [...new Set(toolkits.map((toolkit) => toolkit.trim()))];
113
+ return unique.length > 0 ? unique : void 0;
114
+ };
115
+ const maybeTransform = async (options, params) => options.transformResult ? options.transformResult(params) : params.value;
116
+ const stringValue = (value) => typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
117
+ const toolkitFromToolSlug = (toolSlug) => {
118
+ const normalized = toolSlug.trim().toLowerCase();
119
+ if (!normalized || normalized.startsWith("composio_")) return void 0;
120
+ for (const [prefix, toolkit] of [
121
+ ["google_calendar_", "googlecalendar"],
122
+ ["google_drive_", "googledrive"],
123
+ ["microsoft_teams_", "microsoftteams"]
124
+ ]) if (normalized.startsWith(prefix)) return toolkit;
125
+ const [prefix] = normalized.split("_");
126
+ return prefix || void 0;
127
+ };
128
+
129
+ //#endregion
130
+ //#region src/pi/connections.ts
131
+ const defaultIsToolkitConnected = (state) => {
132
+ if (!state || typeof state !== "object") return false;
133
+ const record = state;
134
+ if (record.isNoAuth === true || record.is_no_auth === true) return true;
135
+ if (typeof record.isConnected === "boolean") return record.isConnected;
136
+ if (typeof record.is_connected === "boolean") return record.is_connected;
137
+ const connection = record.connection;
138
+ if (connection) {
139
+ if (connection.isActive === true || connection.is_active === true) return true;
140
+ if (connection.status === "ACTIVE") return true;
141
+ if (connection.connectedAccount?.status === "ACTIVE") return true;
142
+ if (connection.connected_account?.status === "ACTIVE") return true;
143
+ }
144
+ if (record.connectedAccount?.status === "ACTIVE") return true;
145
+ if (record.connected_account?.status === "ACTIVE") return true;
146
+ return false;
147
+ };
148
+ const toolkitKeyFromState = (state) => {
149
+ if (!state || typeof state !== "object") return void 0;
150
+ const record = state;
151
+ const toolkit = record.toolkit;
152
+ return stringValue(record.slug) ?? stringValue(record.toolkitSlug) ?? stringValue(record.toolkit_slug) ?? stringValue(record.name) ?? stringValue(toolkit?.slug);
153
+ };
154
+ const normalizeToolkitStateMap = (raw, requestedToolkits) => {
155
+ const byToolkit = /* @__PURE__ */ new Map();
156
+ const addState = (toolkit, state) => {
157
+ if (toolkit) byToolkit.set(toolkit.toLowerCase(), state);
158
+ };
159
+ if (!raw) return byToolkit;
160
+ if (Array.isArray(raw)) {
161
+ raw.forEach((state, index) => addState(toolkitKeyFromState(state) ?? requestedToolkits[index], state));
162
+ return byToolkit;
163
+ }
164
+ if (typeof raw === "object") {
165
+ const record = raw;
166
+ if (Array.isArray(record.items)) {
167
+ record.items.forEach((state, index) => addState(toolkitKeyFromState(state) ?? requestedToolkits[index], state));
168
+ return byToolkit;
169
+ }
170
+ for (const toolkit of requestedToolkits) if (record[toolkit] !== void 0) addState(toolkit, record[toolkit]);
171
+ if (byToolkit.size === 0 && requestedToolkits.length === 1) addState(toolkitKeyFromState(raw) ?? requestedToolkits[0], raw);
172
+ }
173
+ return byToolkit;
174
+ };
175
+ const formatDefaultConnectionResult = (results) => {
176
+ return {
177
+ successful: true,
178
+ data: {
179
+ message: Object.values(results).filter((result) => !result.connected).length === 0 ? "All requested toolkits are connected." : "Connection flow initiated for missing toolkits.",
180
+ results
181
+ },
182
+ error: null
183
+ };
184
+ };
185
+ const inferSessionConnections = (session, options) => {
186
+ if (!session.toolkits && !session.authorize) return void 0;
187
+ return {
188
+ getToolkitStates: session.toolkits ? (toolkits) => session.toolkits?.({
189
+ toolkits,
190
+ limit: Math.max(toolkits.length, 1)
191
+ }) : void 0,
192
+ authorizeToolkit: session.authorize ? (toolkit, authorizeOptions) => session.authorize?.(toolkit, {
193
+ callbackUrl: authorizeOptions.callbackUrl ?? options.callbackUrl,
194
+ alias: authorizeOptions.alias,
195
+ experimental: authorizeOptions.experimental
196
+ }) : void 0
197
+ };
198
+ };
199
+ const isCapabilityInput = (value) => "connections" in value || "hooks" in value || "includeWorkbenchTools" in value || "names" in value;
200
+ const toCapabilities = (input, providerOptions, options = {}) => {
201
+ if (isCapabilityInput(input)) return {
202
+ ...providerOptions,
203
+ ...input,
204
+ names: {
205
+ ...providerOptions.names,
206
+ ...input.names ?? {}
207
+ }
208
+ };
209
+ const mergedOptions = {
210
+ ...providerOptions,
211
+ ...options
212
+ };
213
+ return {
214
+ ...mergedOptions,
215
+ sessionId: input.sessionId,
216
+ search: (params) => input.search(params),
217
+ execute: (toolSlug, args, executeOptions) => executeOptions ? input.execute(toolSlug, args, executeOptions) : input.execute(toolSlug, args),
218
+ connections: inferSessionConnections(input, mergedOptions)
219
+ };
220
+ };
221
+
222
+ //#endregion
223
+ //#region src/pi/types.ts
224
+ const DEFAULT_SESSION_TOOL_NAMES = {
225
+ search: "composio_search_tools",
226
+ manageConnections: "composio_manage_connections",
227
+ execute: "composio_execute_tool",
228
+ remoteWorkbench: "composio_remote_workbench",
229
+ remoteBash: "composio_remote_bash"
230
+ };
231
+
232
+ //#endregion
233
+ //#region src/pi/session-tools.ts
234
+ const createPiSessionTools = (input, providerOptions, options = {}) => {
235
+ const capabilities = toCapabilities(input, providerOptions, options);
236
+ const formatter = capabilities.formatResult ?? defaultFormatResult;
237
+ const catchErrors = capabilities.catchErrors ?? true;
238
+ const names = {
239
+ ...DEFAULT_SESSION_TOOL_NAMES,
240
+ ...capabilities.names ?? {}
241
+ };
242
+ const executionMode = capabilities.executionMode;
243
+ const buildBaseContext = (toolCallId, sourceTool, originalRequest) => ({
244
+ toolCallId,
245
+ sourceTool,
246
+ sessionId: capabilities.sessionId,
247
+ originalRequest
248
+ });
249
+ const manageConnectionsForToolkits = async (toolCallId, originalRequest, toolkits, reinitiateAll = false) => {
250
+ const connectionContext = {
251
+ ...buildBaseContext(toolCallId, names.manageConnections, originalRequest),
252
+ requestedToolkits: toolkits,
253
+ callbackUrl: capabilities.callbackUrl,
254
+ reinitiateAll
255
+ };
256
+ const hookContext = {
257
+ ...hookControls,
258
+ request: {
259
+ toolkits,
260
+ reinitiateAll
261
+ },
262
+ context: connectionContext
263
+ };
264
+ const authLinks = [];
265
+ return {
266
+ value: await runHook(capabilities.hooks?.manageConnections, hookContext, async () => {
267
+ const requestedToolkits = normalizeToolkits(hookContext.request.toolkits) ?? [];
268
+ const shouldReinitiateAll = hookContext.request.reinitiateAll;
269
+ connectionContext.requestedToolkits = requestedToolkits;
270
+ connectionContext.reinitiateAll = shouldReinitiateAll;
271
+ const states = normalizeToolkitStateMap(await capabilities.connections?.getToolkitStates?.(requestedToolkits, connectionContext), requestedToolkits);
272
+ const results = {};
273
+ for (const toolkit of requestedToolkits) {
274
+ const state = states.get(toolkit.toLowerCase());
275
+ if ((state ? capabilities.connections?.isConnected?.(state, {
276
+ toolkit,
277
+ request: connectionContext
278
+ }) ?? defaultIsToolkitConnected(state) : false) && !shouldReinitiateAll) {
279
+ results[toolkit] = {
280
+ toolkit,
281
+ connected: true,
282
+ status: "connected",
283
+ state
284
+ };
285
+ continue;
286
+ }
287
+ if (!capabilities.connections?.authorizeToolkit) {
288
+ results[toolkit] = {
289
+ toolkit,
290
+ connected: false,
291
+ status: "missing_authorize_handler",
292
+ state
293
+ };
294
+ continue;
295
+ }
296
+ const authorization = await capabilities.connections.authorizeToolkit(toolkit, {
297
+ callbackUrl: capabilities.callbackUrl,
298
+ reinitiate: shouldReinitiateAll
299
+ }, connectionContext);
300
+ const handledAuthorization = await applyAuthLinkHandlers(capabilities, authorization, {
301
+ ...connectionContext,
302
+ toolkit,
303
+ result: authorization
304
+ });
305
+ authLinks.push(...handledAuthorization.authLinks);
306
+ results[toolkit] = {
307
+ toolkit,
308
+ connected: false,
309
+ status: "auth_initiated",
310
+ state,
311
+ authorization: handledAuthorization.value
312
+ };
313
+ }
314
+ return formatDefaultConnectionResult(results);
315
+ }),
316
+ authLinks,
317
+ context: connectionContext
318
+ };
319
+ };
320
+ const executeWithPolicy = async (toolCallId, sourceTool, originalRequest, toolSlug, args, account) => {
321
+ const executeContext = {
322
+ ...buildBaseContext(toolCallId, sourceTool, originalRequest),
323
+ toolSlug,
324
+ toolkit: toolkitFromToolSlug(toolSlug),
325
+ args,
326
+ account
327
+ };
328
+ const authLinks = [];
329
+ const hookContext = {
330
+ ...hookControls,
331
+ request: {
332
+ toolSlug,
333
+ args,
334
+ account
335
+ },
336
+ context: executeContext,
337
+ manageConnections: async (managedToolkits, manageOptions) => {
338
+ const managed = await manageConnectionsForToolkits(toolCallId, originalRequest, managedToolkits, manageOptions?.reinitiateAll);
339
+ authLinks.push(...managed.authLinks);
340
+ return managed.value;
341
+ }
342
+ };
343
+ return {
344
+ value: await runHook(capabilities.hooks?.execute, hookContext, async () => {
345
+ const finalToolSlug = hookContext.request.toolSlug;
346
+ const finalArgs = hookContext.request.args;
347
+ const finalAccount = hookContext.request.account;
348
+ const finalContext = {
349
+ ...executeContext,
350
+ toolSlug: finalToolSlug,
351
+ toolkit: toolkitFromToolSlug(finalToolSlug),
352
+ args: finalArgs,
353
+ account: finalAccount
354
+ };
355
+ hookContext.context = finalContext;
356
+ const execute = hookContext.request.execute ?? capabilities.execute;
357
+ const session = hookContext.request.session;
358
+ const result = session ? await session.execute(finalToolSlug, finalArgs, finalAccount ? { account: finalAccount } : void 0) : await execute(finalToolSlug, finalArgs, finalAccount ? { account: finalAccount } : void 0, finalContext);
359
+ const handledResult = await applyAuthLinkHandlers(capabilities, result, {
360
+ ...finalContext,
361
+ result
362
+ });
363
+ authLinks.push(...handledResult.authLinks);
364
+ return handledResult.value;
365
+ }),
366
+ authLinks,
367
+ context: hookContext.context
368
+ };
369
+ };
370
+ const searchTools = defineTool({
371
+ name: names.search,
372
+ label: "Composio Search Tools",
373
+ description: "Search Composio for tools that can perform a requested action. Search globally by default; pass toolkits only when intentionally narrowing the search.",
374
+ promptSnippet: "Use composio_search_tools to discover exact Composio tool slugs and schemas before executing app actions.",
375
+ promptGuidelines: ["Search Composio before inventing tool slugs or arguments.", "Only pass a toolkit filter when you intentionally want to narrow search results."],
376
+ parameters: Type.Object({
377
+ query: Type.String({ description: "Natural language description of the action to perform." }),
378
+ toolkits: ToolkitsSchema
379
+ }),
380
+ ...executionMode ? { executionMode } : {},
381
+ execute: async (toolCallId, params) => {
382
+ try {
383
+ const requestedToolkits = normalizeToolkits(params.toolkits);
384
+ const searchContext = {
385
+ ...buildBaseContext(toolCallId, names.search, params),
386
+ query: params.query,
387
+ requestedToolkits
388
+ };
389
+ const hookContext = {
390
+ ...hookControls,
391
+ request: {
392
+ query: params.query,
393
+ ...requestedToolkits ? { toolkits: requestedToolkits } : {}
394
+ },
395
+ context: searchContext
396
+ };
397
+ const authLinks = [];
398
+ const value = await runHook(capabilities.hooks?.search, hookContext, async () => {
399
+ const toolkits = normalizeToolkits(hookContext.request.toolkits);
400
+ hookContext.context.query = hookContext.request.query;
401
+ hookContext.context.requestedToolkits = toolkits;
402
+ const result = await capabilities.search({
403
+ query: hookContext.request.query,
404
+ ...toolkits ? { toolkits } : {}
405
+ }, hookContext.context);
406
+ const handledResult = await applyAuthLinkHandlers(capabilities, result, {
407
+ ...hookContext.context,
408
+ result
409
+ });
410
+ authLinks.push(...handledResult.authLinks);
411
+ return handledResult.value;
412
+ });
413
+ return toPiResult(await maybeTransform(capabilities, {
414
+ tool: "search",
415
+ requestedToolkits: hookContext.context.requestedToolkits,
416
+ value,
417
+ context: hookContext.context
418
+ }), formatter, {
419
+ slug: names.search,
420
+ authLinks
421
+ });
422
+ } catch (error) {
423
+ if (!catchErrors) throw error;
424
+ return toPiErrorResult(error, formatter, { slug: names.search });
425
+ }
426
+ }
427
+ });
428
+ const manageConnections = defineTool({
429
+ name: names.manageConnections,
430
+ label: "Composio Manage Connections",
431
+ description: "Check whether the user has active connections for requested toolkits and initiate Composio auth when needed.",
432
+ promptSnippet: "Use composio_manage_connections when a searched tool requires a missing app connection.",
433
+ promptGuidelines: ["When an app connection is missing, call composio_manage_connections with the toolkit slug.", "Never ask the user for OAuth secrets or API keys directly."],
434
+ parameters: Type.Object({
435
+ toolkits: Type.Array(Type.String({ description: "Toolkit slugs to check/connect, e.g. github, gmail." })),
436
+ reinitiate_all: Type.Optional(Type.Boolean({ description: "Force reconnection even if active connections exist." }))
437
+ }),
438
+ ...executionMode ? { executionMode } : {},
439
+ execute: async (toolCallId, params) => {
440
+ const toolkits = normalizeToolkits(params.toolkits) ?? [];
441
+ try {
442
+ const managed = await manageConnectionsForToolkits(toolCallId, params, toolkits, params.reinitiate_all ?? false);
443
+ return toPiResult(await maybeTransform(capabilities, {
444
+ tool: "manageConnections",
445
+ requestedToolkits: toolkits,
446
+ value: managed.value,
447
+ context: managed.context
448
+ }), formatter, {
449
+ slug: names.manageConnections,
450
+ authLinks: managed.authLinks,
451
+ denied: managed.denied
452
+ });
453
+ } catch (error) {
454
+ if (!catchErrors) throw error;
455
+ return toPiErrorResult(error, formatter, { slug: names.manageConnections });
456
+ }
457
+ }
458
+ });
459
+ const executeTool = defineTool({
460
+ name: names.execute,
461
+ label: "Composio Execute Tool",
462
+ description: "Execute an exact Composio tool slug using the configured Composio execution capability. Use search first so the slug and arguments match the schema.",
463
+ promptSnippet: "Use composio_execute_tool to execute an exact Composio tool slug returned by composio_search_tools.",
464
+ promptGuidelines: ["Always use exact tool slugs and schema-compliant arguments.", "For missing connections, use composio_manage_connections instead of asking for credentials."],
465
+ parameters: Type.Object({
466
+ toolSlug: Type.String({ description: "Exact Composio tool slug, e.g. GITHUB_CREATE_ISSUE." }),
467
+ arguments: optionalRecordSchema("Tool arguments matching the searched schema."),
468
+ account: Type.Optional(Type.String({ description: "Optional account selector for multi-account sessions. Use connected account id or alias when required." }))
469
+ }),
470
+ ...executionMode ? { executionMode } : {},
471
+ prepareArguments: (args) => normalizeToolArguments(args, names.execute),
472
+ execute: async (toolCallId, params) => {
473
+ const toolSlug = params.toolSlug.trim();
474
+ const args = params.arguments ?? {};
475
+ try {
476
+ const executed = await executeWithPolicy(toolCallId, names.execute, params, toolSlug, args, params.account);
477
+ return toPiResult(await maybeTransform(capabilities, {
478
+ tool: "execute",
479
+ requestedToolkits: toolkitFromToolSlug(executed.context.toolSlug) ? [toolkitFromToolSlug(executed.context.toolSlug)] : void 0,
480
+ value: executed.value,
481
+ context: executed.context
482
+ }), formatter, {
483
+ slug: executed.context.toolSlug || names.execute,
484
+ authLinks: executed.authLinks,
485
+ denied: executed.denied
486
+ });
487
+ } catch (error) {
488
+ if (!catchErrors) throw error;
489
+ return toPiErrorResult(error, formatter, { slug: toolSlug || names.execute });
490
+ }
491
+ }
492
+ });
493
+ if (!capabilities.includeWorkbenchTools) return [
494
+ searchTools,
495
+ manageConnections,
496
+ executeTool
497
+ ];
498
+ return [
499
+ searchTools,
500
+ manageConnections,
501
+ executeTool,
502
+ defineTool({
503
+ name: names.remoteWorkbench,
504
+ label: "Composio Remote Workbench",
505
+ description: "Execute Python code inside the Composio remote workbench for this Tool Router session. Use it for remote files, bulk processing, large tool outputs, and Composio-authenticated scripting.",
506
+ promptSnippet: "Use composio_remote_workbench for Python scripting in the Composio remote sandbox when data is large, stored in remote files, or needs session-authenticated tool/proxy helpers.",
507
+ promptGuidelines: ["Use composio_remote_workbench for large data processing or remote workbench files; do not use it for tiny inline transformations.", "Split long-running work into small cells and save checkpoints in the workbench filesystem."],
508
+ parameters: Type.Object({
509
+ code_to_execute: Type.String({ description: "Python code to run in the persistent Composio remote workbench. Keep cells focused and avoid long-running jobs." }),
510
+ timeout: Type.Optional(Type.Number({
511
+ minimum: 1,
512
+ maximum: 780,
513
+ description: "Maximum seconds to allow execution. Defaults to the session/backend workbench timeout."
514
+ })),
515
+ thought: Type.Optional(Type.String({ description: "Concise objective for why this workbench cell is needed." })),
516
+ file_path: Type.Optional(Type.String({ description: "Remote workbench path/glob to analyze when processing a file." })),
517
+ disabled_tools: Type.Optional(Type.Array(Type.String({ description: "Tool slugs to disable for this workbench call." }))),
518
+ session_id: Type.Optional(Type.String({ description: "Workbench workflow session id. Defaults to the Composio Tool Router session id." }))
519
+ }),
520
+ ...executionMode ? { executionMode } : {},
521
+ execute: async (toolCallId, params) => {
522
+ try {
523
+ const request = {
524
+ ...params,
525
+ ...params.session_id || capabilities.sessionId ? { session_id: params.session_id ?? capabilities.sessionId } : {}
526
+ };
527
+ const hookContext = {
528
+ ...hookControls,
529
+ request,
530
+ context: {
531
+ ...buildBaseContext(toolCallId, names.remoteWorkbench, params),
532
+ toolSlug: "COMPOSIO_REMOTE_WORKBENCH",
533
+ args: request
534
+ }
535
+ };
536
+ const authLinks = [];
537
+ const details = {};
538
+ return toPiResult(await maybeTransform(capabilities, {
539
+ tool: "remoteWorkbench",
540
+ value: await runHook(capabilities.hooks?.remoteWorkbench, hookContext, async () => {
541
+ const executed = await executeWithPolicy(toolCallId, names.remoteWorkbench, params, "COMPOSIO_REMOTE_WORKBENCH", hookContext.request);
542
+ authLinks.push(...executed.authLinks);
543
+ details.denied = executed.denied;
544
+ hookContext.context = executed.context;
545
+ return executed.value;
546
+ }),
547
+ context: hookContext.context
548
+ }), formatter, {
549
+ slug: hookContext.context.toolSlug,
550
+ authLinks,
551
+ denied: details.denied
552
+ });
553
+ } catch (error) {
554
+ if (!catchErrors) throw error;
555
+ return toPiErrorResult(error, formatter, { slug: "COMPOSIO_REMOTE_WORKBENCH" });
556
+ }
557
+ }
558
+ }),
559
+ defineTool({
560
+ name: names.remoteBash,
561
+ label: "Composio Remote Bash",
562
+ description: "Execute a bash command inside the Composio remote workbench for this Tool Router session.",
563
+ promptSnippet: "Use composio_remote_bash to inspect or manipulate files in the Composio remote workbench filesystem.",
564
+ promptGuidelines: ["Use composio_remote_bash for filesystem inspection in the remote workbench, especially for truncated output files.", "Keep commands short and non-interactive."],
565
+ parameters: Type.Object({
566
+ command: Type.String({ description: "Bash command to execute in the Composio remote workbench." }),
567
+ session_id: Type.Optional(Type.String({ description: "Workbench workflow session id. Defaults to the Composio Tool Router session id." }))
568
+ }),
569
+ ...executionMode ? { executionMode } : {},
570
+ execute: async (toolCallId, params) => {
571
+ try {
572
+ const request = {
573
+ ...params,
574
+ ...params.session_id || capabilities.sessionId ? { session_id: params.session_id ?? capabilities.sessionId } : {}
575
+ };
576
+ const hookContext = {
577
+ ...hookControls,
578
+ request,
579
+ context: {
580
+ ...buildBaseContext(toolCallId, names.remoteBash, params),
581
+ toolSlug: "COMPOSIO_REMOTE_BASH_TOOL",
582
+ args: request
583
+ }
584
+ };
585
+ const authLinks = [];
586
+ const details = {};
587
+ return toPiResult(await maybeTransform(capabilities, {
588
+ tool: "remoteBash",
589
+ value: await runHook(capabilities.hooks?.remoteBash, hookContext, async () => {
590
+ const executed = await executeWithPolicy(toolCallId, names.remoteBash, params, "COMPOSIO_REMOTE_BASH_TOOL", hookContext.request);
591
+ authLinks.push(...executed.authLinks);
592
+ details.denied = executed.denied;
593
+ hookContext.context = executed.context;
594
+ return executed.value;
595
+ }),
596
+ context: hookContext.context
597
+ }), formatter, {
598
+ slug: hookContext.context.toolSlug,
599
+ authLinks,
600
+ denied: details.denied
601
+ });
602
+ } catch (error) {
603
+ if (!catchErrors) throw error;
604
+ return toPiErrorResult(error, formatter, { slug: "COMPOSIO_REMOTE_BASH_TOOL" });
605
+ }
606
+ }
607
+ })
608
+ ];
609
+ };
610
+
611
+ //#endregion
612
+ //#region src/pi/provider.ts
613
+ /**
614
+ * Provider for integrating Composio tools with Pi SDK custom tools.
615
+ */
616
+ var PiProvider = class extends BaseAgenticProvider {
617
+ options;
618
+ name = "pi";
619
+ constructor(options = {}) {
620
+ super();
621
+ this.options = options;
622
+ }
623
+ /**
624
+ * Wrap a concrete Composio tool as a Pi custom tool definition.
625
+ */
626
+ wrapTool(composioTool, executeTool) {
627
+ const formatter = this.options.formatResult ?? defaultFormatResult;
628
+ const catchErrors = this.options.catchErrors ?? true;
629
+ const schema = objectInputSchema(composioTool.inputParameters);
630
+ return defineTool({
631
+ name: composioTool.slug,
632
+ label: `${this.options.labelPrefix ?? "Composio"}: ${composioTool.name ?? composioTool.slug}`,
633
+ description: composioTool.description ?? `Execute ${composioTool.slug} with Composio.`,
634
+ promptSnippet: `Use ${composioTool.slug} for ${composioTool.description ?? composioTool.name ?? "this Composio action"}.`,
635
+ parameters: schema,
636
+ ...this.options.executionMode ? { executionMode: this.options.executionMode } : {},
637
+ prepareArguments: (args) => normalizeToolArguments(args, composioTool.slug),
638
+ execute: async (_toolCallId, params) => {
639
+ try {
640
+ const args = normalizeToolArguments(params, composioTool.slug);
641
+ const result = await executeTool(composioTool.slug, args);
642
+ return toPiResult(result, formatter, {
643
+ slug: composioTool.slug,
644
+ error: result?.error
645
+ });
646
+ } catch (error) {
647
+ if (!catchErrors) throw error;
648
+ return toPiErrorResult(error, formatter, { slug: composioTool.slug });
649
+ }
650
+ }
651
+ });
652
+ }
653
+ /**
654
+ * Wrap multiple concrete Composio tools as Pi custom tools.
655
+ */
656
+ wrapTools(tools, executeTool) {
657
+ return tools.map((tool) => this.wrapTool(tool, executeTool));
658
+ }
659
+ /**
660
+ * Create Slack-bot-style dynamic Composio helpers.
661
+ *
662
+ * Prefer passing capabilities (`search`, `execute`, `connections`, `hooks`)
663
+ * so app code owns auth, interception, and shared/service-session routing.
664
+ * Passing a native Tool Router session is also supported; connection
665
+ * management uses `session.toolkits()` + `session.authorize()` when present
666
+ * and never executes `COMPOSIO_MANAGE_CONNECTIONS` internally.
667
+ */
668
+ createSessionTools(input, options = {}) {
669
+ return createPiSessionTools(input, this.options, options);
670
+ }
671
+ /**
672
+ * Transform MCP URL responses into Pi-compatible standard URL entries.
673
+ */
674
+ wrapMcpServerResponse(data) {
675
+ return data.map((item) => ({
676
+ url: new URL(item.url),
677
+ name: item.name
678
+ }));
679
+ }
680
+ };
681
+
682
+ //#endregion
683
+ //#region src/pi/prompt.ts
684
+ const createPiComposioSystemPrompt = (sessionId, options = {}) => [
685
+ "You have Composio tools for working across the user's connected apps.",
686
+ "Use composio_search_tools to find the right tool before executing app actions.",
687
+ "Use composio_manage_connections when an app is not connected; never ask for OAuth secrets or API keys.",
688
+ "Use composio_execute_tool with exact tool slugs and schema-compliant arguments from search results.",
689
+ options.includeWorkbenchTools ? "Use composio_remote_workbench or composio_remote_bash for large outputs, remote files, or Composio-authenticated scripting in the remote workbench." : void 0,
690
+ sessionId ? `Composio session id: ${sessionId}` : void 0
691
+ ].filter(Boolean).join("\n");
692
+
693
+ //#endregion
694
+ export { DEFAULT_SESSION_TOOL_NAMES as PI_COMPOSIO_SESSION_TOOL_NAMES, PiProvider, createPiComposioSystemPrompt, denyPiToolCall, extractComposioConnectLinks };
@@ -0,0 +1,24 @@
1
+ import { Composio, Session } from "@composio/core";
2
+
3
+ //#region src/workbench/types.d.ts
4
+ interface LocalWorkbenchSession {
5
+ helperSource: string;
6
+ env: Record<string, string>;
7
+ }
8
+ //#endregion
9
+ //#region src/workbench/local-workbench.d.ts
10
+ declare function experimental_createLocalWorkbenchSession(composio: Composio, session: Session<unknown, unknown, never>): Promise<LocalWorkbenchSession>;
11
+ //#endregion
12
+ //#region src/workbench/shim.d.ts
13
+ interface WorkbenchEnvOptions {
14
+ sessionId: string;
15
+ backendUrl: string;
16
+ apiKey: string;
17
+ }
18
+ interface PythonWorkbenchHelperSourceOptions {
19
+ invokeLlmModel?: string;
20
+ }
21
+ declare function experimental_createWorkbenchEnv(env: WorkbenchEnvOptions): Record<string, string>;
22
+ declare function experimental_createPythonWorkbenchHelperSource(opts?: PythonWorkbenchHelperSourceOptions): string;
23
+ //#endregion
24
+ export { type LocalWorkbenchSession, type PythonWorkbenchHelperSourceOptions, type WorkbenchEnvOptions, experimental_createLocalWorkbenchSession, experimental_createPythonWorkbenchHelperSource, experimental_createWorkbenchEnv };
@@ -0,0 +1,40 @@
1
+ //#region src/workbench/python-helpers.generated.ts
2
+ const PYTHON_WORKBENCH_HELPER_SOURCE = "import json\nimport os\nimport random\nimport time\nimport urllib.error\nimport urllib.parse\nimport urllib.request\nimport uuid\nfrom typing import Any, Dict, Literal, Optional\n\n\n# Config is injected by the SDK via an `_INTERNAL` dict in a prologue prepended\n# at runtime. When running this file directly (e.g. pytest), default to empty.\ntry:\n _INTERNAL\nexcept NameError:\n _INTERNAL = {}\n\n\nDEFAULT_INVOKE_LLM_MODEL = _INTERNAL.get(\"invoke_llm_model\", \"openai/gpt-oss-120b\")\nRATE_LIMIT_PATTERNS = (\n \"rate limit\",\n \"ratelimit\",\n \"too many requests\",\n \"quota exceeded\",\n \"resource exhausted\",\n)\n\n\ndef _read_env(name, default=None):\n value = os.environ.get(name)\n return default if value is None or value == \"\" else value\n\n\ndef _require_value(value, label):\n if value is None or value == \"\":\n raise RuntimeError(\"%s is required\" % label)\n return value\n\n\ndef _request_id():\n return str(uuid.uuid4())\n\n\ndef _session_execute_url():\n backend_url = _read_env(\"BACKEND_URL\", \"https://backend.composio.dev\").rstrip(\"/\")\n session_id = _require_value(\n _read_env(\"COMPOSIO_TOOLROUTER_SESSION_ID\"),\n \"COMPOSIO_TOOLROUTER_SESSION_ID\",\n )\n encoded_session_id = urllib.parse.quote(session_id, safe=\"\")\n return \"%s/api/v3/tool_router/session/%s/execute\" % (backend_url, encoded_session_id)\n\n\ndef _session_proxy_execute_url():\n backend_url = _read_env(\"BACKEND_URL\", \"https://backend.composio.dev\").rstrip(\"/\")\n session_id = _require_value(\n _read_env(\"COMPOSIO_TOOLROUTER_SESSION_ID\"),\n \"COMPOSIO_TOOLROUTER_SESSION_ID\",\n )\n encoded_session_id = urllib.parse.quote(session_id, safe=\"\")\n return \"%s/api/v3/tool_router/session/%s/proxy_execute\" % (\n backend_url,\n encoded_session_id,\n )\n\n\ndef _post_json(url, headers, payload, timeout=120):\n body = json.dumps(payload).encode(\"utf-8\")\n request = urllib.request.Request(url, data=body, headers=headers, method=\"POST\")\n try:\n with urllib.request.urlopen(request, timeout=timeout) as response:\n return response.status, dict(response.headers), response.read().decode(\"utf-8\")\n except urllib.error.HTTPError as error:\n return error.code, dict(error.headers), error.read().decode(\"utf-8\")\n\n\ndef _parse_json(text):\n if not text:\n return {}\n return json.loads(text)\n\n\ndef _safe_json(text):\n try:\n return _parse_json(text)\n except json.JSONDecodeError:\n return {\"raw\": text}\n\n\ndef _contains_rate_limit_error(payload):\n # Only inspect the API \"error\" field, not the whole body — otherwise benign\n # tool output mentioning \"rate limit\"/\"quota\" triggers spurious retries.\n if not isinstance(payload, dict):\n return False\n error = payload.get(\"error\")\n if not error:\n return False\n text = json.dumps(error, default=str).lower()\n return any(pattern in text for pattern in RATE_LIMIT_PATTERNS)\n\n\ndef _retry_delay(attempt, delay_ms):\n base_delay = max(delay_ms, 0) / 1000.0\n if base_delay == 0:\n return\n # Exponential backoff: double the delay each attempt (parity with Apollo).\n backoff = base_delay * (2 ** attempt)\n jitter = random.uniform(0, min(backoff * 0.2, 0.5))\n time.sleep(backoff + jitter)\n\n\ndef _json_shape(value):\n if isinstance(value, dict):\n return {key: _json_shape(item) for key, item in value.items()}\n if isinstance(value, list):\n return [_json_shape(value[0])] if value else []\n if value is None:\n return \"null\"\n return type(value).__name__\n\n\ndef print_json_structure(value):\n print(json.dumps(_json_shape(value), indent=2, sort_keys=True))\n\n\ndef _track_helper_event(*_args, **_kwargs):\n return None\n\n\ndef run_composio_tool(\n tool_slug,\n arguments=None,\n retry_params=None,\n print_schema_for_tool=True,\n *,\n account=None,\n):\n if not tool_slug:\n return {}, \"tool_slug is required\"\n\n api_key = _require_value(_read_env(\"COMPOSIO_API_KEY\"), \"COMPOSIO_API_KEY\")\n retry_config = {\"max_retries\": 3, \"delay_ms\": 2000}\n if retry_params:\n retry_config.update(retry_params)\n\n payload = {\n \"tool_slug\": str(tool_slug).strip().upper(),\n \"arguments\": arguments or {},\n }\n if account is not None:\n payload[\"account\"] = account\n\n headers = {\n \"Content-Type\": \"application/json\",\n \"x-api-key\": api_key,\n \"x-request-id\": _request_id(),\n }\n max_retries = int(retry_config.get(\"max_retries\", 3))\n delay_ms = int(retry_config.get(\"delay_ms\", 2000))\n\n for attempt in range(max_retries + 1):\n try:\n status, _headers, text = _post_json(_session_execute_url(), headers, payload)\n except (urllib.error.URLError, TimeoutError) as error:\n # Network failure (timeout, connection/DNS error). Retry transient\n # failures, then surface as the error tuple instead of throwing.\n if attempt < max_retries:\n _retry_delay(attempt, delay_ms)\n continue\n return {}, \"Composio tool request failed: %s\" % error\n\n if status == 429 and attempt < max_retries:\n _retry_delay(attempt, delay_ms)\n continue\n\n if status >= 400:\n response_data = _safe_json(text)\n return response_data, \"Composio tool execution failed with HTTP %s\" % status\n\n try:\n response_data = _parse_json(text)\n except json.JSONDecodeError as error:\n return {\"raw\": text}, \"Failed to parse Composio tool response as JSON: %s\" % error\n\n if _contains_rate_limit_error(response_data) and attempt < max_retries:\n _retry_delay(attempt, delay_ms)\n continue\n\n # A failed tool call returns HTTP 200 with a top-level \"error\" field;\n # surface it as the error tuple element so callers don't read it as success.\n if isinstance(response_data, dict) and response_data.get(\"error\"):\n return response_data, str(response_data[\"error\"])\n\n if print_schema_for_tool:\n print_json_structure(response_data)\n return response_data, \"\"\n\n return {}, \"Composio tool execution failed after retries\"\n\n\ndef _strip_code_fence(content):\n stripped = content.strip()\n code_fence = chr(96) * 3\n if stripped.startswith(code_fence):\n stripped = stripped[3:].strip()\n if stripped.lower().startswith(\"json\"):\n stripped = stripped[4:].strip()\n if stripped.endswith(code_fence):\n stripped = stripped[:-3].strip()\n elif stripped.lower().startswith(\"json\\n\"):\n stripped = stripped[5:].strip()\n return stripped.strip()\n\n\ndef invoke_llm(query, reasoning_effort=None):\n if not query:\n return \"\", \"query is required\"\n\n system_prompt = (\n \"You are a generic, smart large language model. \"\n \"When asked to output a JSON, respond only with valid JSON - \"\n \"no other text / code fences / quotes around the JSON.\"\n )\n response, error = run_composio_tool(\n \"COMPOSIO_SEARCH_GROQ_CHAT\",\n {\n \"model\": DEFAULT_INVOKE_LLM_MODEL,\n \"messages\": [\n {\"role\": \"system\", \"content\": system_prompt},\n {\"role\": \"user\", \"content\": query},\n ],\n \"temperature\": 0.5,\n },\n None,\n False,\n )\n _track_helper_event(\"invoke_llm\", {\"reasoning_effort\": reasoning_effort})\n\n if error:\n return \"\", error\n\n choices = (response.get(\"data\") or {}).get(\"choices\", \"\")\n if not choices:\n return \"\", \"No choices returned from invoke_llm\"\n\n first_choice = choices[0] if isinstance(choices, list) else choices\n content = (first_choice.get(\"message\") or {}).get(\"content\", \"\")\n if not content:\n return \"\", \"No content returned from invoke_llm\"\n return _strip_code_fence(content), \"\"\n\n\ndef web_search(query):\n if not query:\n return \"\", \"query is required\"\n\n response, error = run_composio_tool(\n \"COMPOSIO_SEARCH_EXA_ANSWER\",\n {\"content\": query},\n None,\n False,\n )\n _track_helper_event(\"web_search\", {})\n\n if error:\n return \"\", error\n return (response.get(\"data\") or {}).get(\"answer\", \"\"), \"\"\n\n\ndef proxy_execute(\n method: Literal[\"GET\", \"POST\", \"PUT\", \"DELETE\", \"PATCH\"],\n endpoint: str,\n toolkit: str,\n query_params: Optional[Dict[str, str]] = None,\n body: Optional[object] = None,\n headers: Optional[Dict[str, str]] = None,\n) -> tuple[Any, str]:\n \"\"\"Call a toolkit's API directly when no Composio tool exists.\n\n The session resolves the connected account and injects auth server-side, so\n your code never handles raw credentials.\n\n Args:\n method: HTTP method to use for the request. Example: \"GET\"\n endpoint: API endpoint to call. Example: \"/repos/owner/repo\"\n toolkit: Name of the toolkit. Example: \"GITHUB\"\n query_params: Query parameters as key-value pairs. Example: {\"q\": \"is:unread\"}\n body: The request body (required for POST, PUT, and PATCH requests)\n headers: HTTP headers as key-value pairs. Example: {\"Accept\": \"application/json\"}\n\n Returns:\n tuple[Any, str]:\n - data: Response data from the API (None if error)\n - error: Error message (\"\" if no error)\n \"\"\"\n # Wrapped end-to-end so the helper always returns a (data, error) tuple\n # rather than raising.\n try:\n valid_methods = [\"GET\", \"POST\", \"PUT\", \"DELETE\", \"PATCH\"]\n if method.upper() not in valid_methods:\n return None, \"Invalid HTTP method: \" + method\n\n if not endpoint.strip():\n return None, \"Endpoint cannot be empty\"\n\n if not toolkit.strip():\n return None, \"Toolkit cannot be empty\"\n\n if query_params is not None and not isinstance(query_params, dict):\n return None, (\n \"Invalid query_params type: expected dict or None, got \"\n + type(query_params).__name__\n )\n\n if headers is not None and not isinstance(headers, dict):\n return None, (\n \"Invalid headers type: expected dict or None, got \"\n + type(headers).__name__\n )\n\n if method.upper() in [\"POST\", \"PUT\", \"PATCH\"] and body is None:\n return None, \"Body is required for \" + method.upper() + \" requests\"\n\n if method.upper() in [\"GET\", \"DELETE\"] and body is not None:\n return None, \"Body should not be provided for \" + method.upper() + \" requests\"\n\n api_key = _read_env(\"COMPOSIO_API_KEY\")\n if not api_key:\n return None, \"Missing environment variable COMPOSIO_API_KEY\"\n if not _read_env(\"COMPOSIO_TOOLROUTER_SESSION_ID\"):\n return None, \"Missing environment variable COMPOSIO_TOOLROUTER_SESSION_ID\"\n\n # Convert query_params and headers dicts to the flat parameters array.\n parameters = []\n if query_params:\n for key, value in query_params.items():\n parameters.append({\"name\": key, \"value\": str(value), \"type\": \"query\"})\n if headers:\n for key, value in headers.items():\n parameters.append({\"name\": key, \"value\": str(value), \"type\": \"header\"})\n\n payload = {\n \"toolkit_slug\": toolkit.lower(),\n \"endpoint\": endpoint,\n \"method\": method.upper(),\n }\n if parameters:\n payload[\"parameters\"] = parameters\n if body is not None:\n payload[\"body\"] = body\n\n request_headers = {\n \"Content-Type\": \"application/json\",\n \"x-api-key\": api_key,\n \"x-request-id\": _request_id(),\n }\n\n status, _response_headers, text = _post_json(\n _session_proxy_execute_url(), request_headers, payload\n )\n\n # Handle HTTP errors explicitly. Always surface a non-empty message so a\n # >=400 response never looks like success (an empty error/message field\n # must not win over the default).\n if status >= 400:\n error_msg = \"HTTP \" + str(status) + \" Error\"\n try:\n error_data = json.loads(text)\n if isinstance(error_data, dict):\n error_msg = (\n error_data.get(\"error\") or error_data.get(\"message\") or error_msg\n )\n except (ValueError, TypeError):\n if text:\n sanitized = text[:200].replace(\"\\n\", \" \").strip()\n error_msg = \"HTTP \" + str(status) + \": \" + sanitized\n return None, str(error_msg)\n\n response_data = json.loads(text)\n\n # The session wraps the proxied response as {data, status, headers}; the\n # toolkit's own API may still report a >=400 status inside that envelope.\n # `status` can arrive as a string or null, so coerce it before comparing\n # — a bare `>= 400` against a non-int would raise and mask a valid result.\n if isinstance(response_data, dict):\n try:\n api_status = int(response_data.get(\"status\", 200))\n except (TypeError, ValueError):\n api_status = 200\n if api_status >= 400:\n error_msg = \"API request failed\"\n if isinstance(response_data.get(\"data\"), dict):\n error_msg = response_data[\"data\"].get(\n \"message\", response_data[\"data\"].get(\"error\", error_msg)\n )\n return response_data, \"API returned status \" + str(api_status) + \": \" + str(error_msg)\n return response_data.get(\"data\"), \"\"\n\n return response_data, \"\"\n except Exception as error:\n return None, \"Failed to execute proxy request: \" + str(error)\n";
3
+
4
+ //#endregion
5
+ //#region src/workbench/shim.ts
6
+ function trimTrailingSlashes(value) {
7
+ let end = value.length;
8
+ while (end > 0 && value.charCodeAt(end - 1) === 47) end -= 1;
9
+ return value.slice(0, end);
10
+ }
11
+ function experimental_createWorkbenchEnv(env) {
12
+ return {
13
+ BACKEND_URL: trimTrailingSlashes(env.backendUrl),
14
+ COMPOSIO_TOOLROUTER_SESSION_ID: env.sessionId,
15
+ COMPOSIO_API_KEY: env.apiKey
16
+ };
17
+ }
18
+ function experimental_createPythonWorkbenchHelperSource(opts = {}) {
19
+ const config = { invoke_llm_model: opts.invokeLlmModel ?? "openai/gpt-oss-120b" };
20
+ return `import json as _composio_internal_json\n_INTERNAL = _composio_internal_json.loads(${JSON.stringify(JSON.stringify(config))})\n` + PYTHON_WORKBENCH_HELPER_SOURCE;
21
+ }
22
+
23
+ //#endregion
24
+ //#region src/workbench/local-workbench.ts
25
+ async function experimental_createLocalWorkbenchSession(composio, session) {
26
+ if (session.workbench?.enable !== false) throw new Error("experimental_createLocalWorkbenchSession requires a session created with workbench.enable: false. The remote workbench and a local sandbox cannot both run for one session.");
27
+ const { apiKey, baseURL } = composio.getConfig();
28
+ if (!apiKey) throw new Error("A Composio project API key is required to create a local workbench session");
29
+ return {
30
+ env: experimental_createWorkbenchEnv({
31
+ sessionId: session.sessionId,
32
+ backendUrl: baseURL ?? "https://backend.composio.dev",
33
+ apiKey
34
+ }),
35
+ helperSource: experimental_createPythonWorkbenchHelperSource()
36
+ };
37
+ }
38
+
39
+ //#endregion
40
+ export { experimental_createLocalWorkbenchSession, experimental_createPythonWorkbenchHelperSource, experimental_createWorkbenchEnv };
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@composio/experimental",
3
+ "version": "0.1.0",
4
+ "description": "Experimental Composio integrations and helpers",
5
+ "main": "src/index.ts",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/ComposioHQ/composio.git",
10
+ "directory": "ts/packages/experimental"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/ComposioHQ/composio/issues"
14
+ },
15
+ "homepage": "https://github.com/ComposioHQ/composio/tree/main/ts/packages/experimental#readme",
16
+ "publishConfig": {
17
+ "access": "public",
18
+ "main": "dist/index.mjs",
19
+ "types": "dist/index.d.mts"
20
+ },
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/index.d.mts",
24
+ "default": "./dist/index.mjs"
25
+ },
26
+ "./pi": {
27
+ "types": "./dist/index.d.mts",
28
+ "default": "./dist/index.mjs"
29
+ },
30
+ "./workbench": {
31
+ "types": "./dist/workbench/index.d.mts",
32
+ "default": "./dist/workbench/index.mjs"
33
+ }
34
+ },
35
+ "files": [
36
+ "README.md",
37
+ "dist"
38
+ ],
39
+ "scripts": {
40
+ "clean": "git clean -xdf node_modules",
41
+ "build:python-helpers": "tsx scripts/build-python-helpers.ts",
42
+ "check:python-helpers": "tsx scripts/build-python-helpers.ts && git diff --exit-code -- src/workbench/python-helpers.generated.ts",
43
+ "build": "pnpm run build:python-helpers && pnpm exec tsdown",
44
+ "test": "vitest run",
45
+ "typecheck": "tsc --noEmit -p tsconfig.json"
46
+ },
47
+ "keywords": [
48
+ "composio",
49
+ "experimental",
50
+ "pi",
51
+ "coding-agent",
52
+ "tools",
53
+ "agent"
54
+ ],
55
+ "author": "",
56
+ "license": "ISC",
57
+ "dependencies": {
58
+ "typebox": "1.1.38"
59
+ },
60
+ "peerDependencies": {
61
+ "@composio/core": ">=0.10.0 <1.0.0",
62
+ "@earendil-works/pi-coding-agent": ">=0.79.0 <1.0.0"
63
+ },
64
+ "devDependencies": {
65
+ "@composio/core": "workspace:*",
66
+ "@earendil-works/pi-coding-agent": "0.79.8",
67
+ "tsdown": "catalog:",
68
+ "tsx": "catalog:",
69
+ "typescript": "catalog:",
70
+ "vitest": "catalog:"
71
+ }
72
+ }
package/src/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Experimental Composio integrations and helpers.
3
+ *
4
+ * Currently includes the experimental Pi provider for
5
+ * @earendil-works/pi-coding-agent.
6
+ *
7
+ * @packageDocumentation
8
+ * @module experimental
9
+ */
10
+ export * from './pi/index';