pi-bob 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/PLAN.md +96 -0
- package/README.md +212 -0
- package/extensions/bob.ts +455 -0
- package/package.json +37 -0
package/PLAN.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# IBM Bob provider plan
|
|
2
|
+
|
|
3
|
+
## Safety boundaries
|
|
4
|
+
|
|
5
|
+
- Use only IBM-approved Bob/model endpoints and credentials the user is authorised to use.
|
|
6
|
+
- Do not scrape, extract, copy, or reuse Bob browser/session credentials.
|
|
7
|
+
- Do not store secrets in this repository or in chat transcripts.
|
|
8
|
+
- Prefer environment variables, password-manager commands, formal Pi `/login` support, or approved local auth helpers for credentials.
|
|
9
|
+
|
|
10
|
+
## Implementation strategy
|
|
11
|
+
|
|
12
|
+
### Option 1 — compatible provider first
|
|
13
|
+
|
|
14
|
+
Implement a Pi package that registers IBM Bob as a normal Pi provider using Pi's built-in compatible API adapters:
|
|
15
|
+
|
|
16
|
+
- `openai-completions` for OpenAI Chat Completions-compatible Bob routes.
|
|
17
|
+
- `openai-responses` for OpenAI Responses-compatible Bob routes.
|
|
18
|
+
- `anthropic-messages` for Anthropic Messages-compatible Bob routes.
|
|
19
|
+
|
|
20
|
+
This avoids custom streaming code and keeps the integration transparent: Pi serializes messages, tools, streaming, and usage exactly as it already does for compatible providers.
|
|
21
|
+
|
|
22
|
+
### Option 2 — approved SSO/auth wrapper
|
|
23
|
+
|
|
24
|
+
If Bob uses short-lived IBM SSO/IAM/direct-access tokens but still exposes an OpenAI/Anthropic-compatible model API, add a Pi `/login ibm-bob` flow or an approved auth-helper integration that obtains tokens, injects headers, and delegates to Pi's built-in streamers.
|
|
25
|
+
|
|
26
|
+
Implemented: `/login ibm-bob` uses Bob's browser SSO endpoints (`/authn/v1/auth/token` and `/authn/v1/auth/refresh`) and Pi's standard OAuth credential store. It does not extract Bob Shell's stored token from `~/.bob/settings.json` or Bob's credential store.
|
|
27
|
+
|
|
28
|
+
### Option 3 — native Bob/watsonx adapter
|
|
29
|
+
|
|
30
|
+
Only if Bob exposes a non-compatible native API, implement a full `streamSimple` adapter that maps Pi context and tool calls to Bob requests and Bob streaming events back to Pi assistant-message events.
|
|
31
|
+
|
|
32
|
+
## Discovered local Bob Shell facts
|
|
33
|
+
|
|
34
|
+
From `bobshell@1.0.6` installed at `/opt/homebrew/lib/node_modules/bobshell`:
|
|
35
|
+
|
|
36
|
+
- CLI path: `/opt/homebrew/bin/bob`
|
|
37
|
+
- Local auth method: `sso`
|
|
38
|
+
- Default Bob API host: `https://api.us-east.bob.ibm.com`
|
|
39
|
+
- OpenAI-compatible base URL: `https://api.us-east.bob.ibm.com/inference/v1`
|
|
40
|
+
- Chat route: `/inference/v1/chat/completions`
|
|
41
|
+
- Model-info route: `/inference/v1/model/info`
|
|
42
|
+
- Default model alias: `premium`
|
|
43
|
+
- Other visible aliases/constants: `pro`, `flash`, `flash-lite`, `bob-3-pro-preview`
|
|
44
|
+
- Installed Bob Shell contains a broad ~1M context-window default, but the observed `premium` backend route maps to Claude Sonnet 4.5 with `Max Input Tokens=200000`.
|
|
45
|
+
- Provider default context window for Pi: `200000`, so Pi compacts before Bob rejects oversized requests.
|
|
46
|
+
- Default max output token constant: `8192`
|
|
47
|
+
- Non-secret routing headers: `x-instance-id`, `x-team-id`
|
|
48
|
+
- SSO browser login URL: `https://bob.ibm.com/login?callback_uri=...&state=...`
|
|
49
|
+
- SSO token endpoint: `/authn/v1/auth/token`
|
|
50
|
+
- SSO refresh endpoint: `/authn/v1/auth/refresh`
|
|
51
|
+
|
|
52
|
+
## Option 1 execution
|
|
53
|
+
|
|
54
|
+
This repository now contains a local Pi package:
|
|
55
|
+
|
|
56
|
+
- `package.json` declares a Pi package.
|
|
57
|
+
- `extensions/bob.ts` registers provider `ibm-bob` and Pi `/login ibm-bob` SSO.
|
|
58
|
+
- `README.md` documents discovered settings, setup, and validation.
|
|
59
|
+
|
|
60
|
+
Defaults now target the discovered Bob Shell route:
|
|
61
|
+
|
|
62
|
+
- `IBM_BOB_BASE_URL=https://api.us-east.bob.ibm.com/inference/v1`
|
|
63
|
+
- `IBM_BOB_API=openai-completions`
|
|
64
|
+
- `IBM_BOB_MODELS=premium`
|
|
65
|
+
- `IBM_BOB_CONTEXT_WINDOW=200000`
|
|
66
|
+
- `IBM_BOB_MAX_TOKENS=8192`
|
|
67
|
+
|
|
68
|
+
The extension reads only non-secret `ibm.instanceId` and `ibm.teamId` from `~/.bob/settings.json` unless `IBM_BOB_READ_BOBSHELL_SETTINGS=false`.
|
|
69
|
+
|
|
70
|
+
Secret configuration remains external:
|
|
71
|
+
|
|
72
|
+
- Pi `/login ibm-bob`, which stores fresh SSO credentials in `~/.pi/agent/auth.json`, or
|
|
73
|
+
- `IBM_BOB_API_KEY`, or
|
|
74
|
+
- `IBM_BOB_HEADERS_JSON` with environment-variable placeholders, or
|
|
75
|
+
- future approved local auth-helper support.
|
|
76
|
+
|
|
77
|
+
## Validation completed
|
|
78
|
+
|
|
79
|
+
- Bob Shell SSO test succeeded with `bob --auth-method sso -m premium`.
|
|
80
|
+
- Direct unauthenticated model-info call reached the discovered endpoint and returned expected `401 Authentication required`.
|
|
81
|
+
- Pi provider lists `ibm-bob/premium` with dummy token.
|
|
82
|
+
- Pi request with dummy token reaches Bob and returns expected `401 unauthorized`.
|
|
83
|
+
- Independent Bob SSO smoke test succeeded: callback received, token exchange succeeded, and authenticated model-info returned HTTP `200`.
|
|
84
|
+
- Direct minimal chat completion returned `direct-bob-ok`.
|
|
85
|
+
- Direct tool payload test showed Bob rejects `tools[].function.strict`; provider default now sets `supportsStrictMode=false`.
|
|
86
|
+
- Pi end-to-end with fresh SSO token succeeded: `pi-bob-ok`.
|
|
87
|
+
- After a real context overflow from Bob (`Max Input Tokens=200000, Got=236458`), the provider default context window was lowered from `1048576` to `200000` so Pi's own compaction/overflow handling can engage before Bob rejects the request.
|
|
88
|
+
|
|
89
|
+
## Remaining work
|
|
90
|
+
|
|
91
|
+
The provider works end-to-end for a basic Pi prompt through Bob's OpenAI-compatible route. Useful next steps:
|
|
92
|
+
|
|
93
|
+
1. Run interactive `/login ibm-bob` once inside Pi to persist SSO credentials in Pi's auth store.
|
|
94
|
+
2. Add `/bob-status` for no-secret health checks.
|
|
95
|
+
3. Add dynamic model discovery from `/inference/v1/model/info`.
|
|
96
|
+
4. Add regression tests for SSO callback, refresh, and strict-tool compatibility.
|
package/README.md
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# pi-bob
|
|
2
|
+
|
|
3
|
+
Pi provider package for IBM Bob / IBM-approved enterprise model endpoints.
|
|
4
|
+
|
|
5
|
+
This first pass implements **Option 1** from `PLAN.md`: register Bob as a Pi provider using Pi's built-in compatible provider APIs. It does not scrape Bob, extract browser/session credentials, or bypass IBM-approved access paths.
|
|
6
|
+
|
|
7
|
+
## Discovered local Bob Shell settings
|
|
8
|
+
|
|
9
|
+
From the installed `bobshell@1.0.6` package and the local redacted Bob configuration:
|
|
10
|
+
|
|
11
|
+
- Bob Shell CLI: `/opt/homebrew/bin/bob`
|
|
12
|
+
- Bob Shell package: `/opt/homebrew/lib/node_modules/bobshell`
|
|
13
|
+
- Installed auth method: `sso`
|
|
14
|
+
- Default Bob API host: `https://api.us-east.bob.ibm.com`
|
|
15
|
+
- OpenAI-compatible chat base URL: `https://api.us-east.bob.ibm.com/inference/v1`
|
|
16
|
+
- Chat completions route used by Bob Shell: `/inference/v1/chat/completions`
|
|
17
|
+
- Model-info route used by Bob Shell: `/inference/v1/model/info`
|
|
18
|
+
- Default model alias used by Bob Shell: `premium`
|
|
19
|
+
- Other visible aliases/constants in the installed client: `pro`, `flash`, `flash-lite`, `bob-3-pro-preview`
|
|
20
|
+
- Installed Bob Shell contains a broad ~1M context-window default, but the observed `premium` backend route maps to Claude Sonnet 4.5 with `Max Input Tokens=200000`.
|
|
21
|
+
- Default context window advertised to Pi: `200000`, so Pi compacts before Bob rejects oversized requests.
|
|
22
|
+
- Default max output token constant in the installed client: `8192`
|
|
23
|
+
|
|
24
|
+
Bob Shell sends non-secret instance/team routing headers. This extension reads only these non-secret fields from `~/.bob/settings.json` by default:
|
|
25
|
+
|
|
26
|
+
- `ibm.instanceId` -> `x-instance-id`
|
|
27
|
+
- `ibm.teamId` -> `x-team-id`
|
|
28
|
+
|
|
29
|
+
It intentionally ignores Bob's stored SSO secrets.
|
|
30
|
+
|
|
31
|
+
## What it supports
|
|
32
|
+
|
|
33
|
+
Set `IBM_BOB_API` to one of Pi's compatible API adapters:
|
|
34
|
+
|
|
35
|
+
- `openai-completions` — default; OpenAI Chat Completions-compatible routes.
|
|
36
|
+
- `openai-responses` — OpenAI Responses-compatible routes.
|
|
37
|
+
- `anthropic-messages` — Anthropic Messages-compatible routes.
|
|
38
|
+
|
|
39
|
+
The extension registers provider id `ibm-bob`.
|
|
40
|
+
|
|
41
|
+
## Quick start for the discovered Bob endpoint
|
|
42
|
+
|
|
43
|
+
Use Bob SSO through Pi:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pi -e /Users/larry.song/work/hashicorp/pi-bob
|
|
47
|
+
# inside Pi:
|
|
48
|
+
# /login ibm-bob
|
|
49
|
+
# /model ibm-bob/premium
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Pi stores the resulting Bob SSO access/refresh tokens in Pi's normal auth store (`~/.pi/agent/auth.json`). This extension obtains those tokens only through Bob's browser SSO endpoints; it does not read Bob Shell's stored SSO secrets.
|
|
53
|
+
|
|
54
|
+
For non-interactive/API-key use, if you have an approved Bob bearer token/API token available outside Bob Shell, run:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
export IBM_BOB_API_KEY="..." # do not commit this
|
|
58
|
+
|
|
59
|
+
pi -e /Users/larry.song/work/hashicorp/pi-bob --list-models
|
|
60
|
+
pi -e /Users/larry.song/work/hashicorp/pi-bob --model ibm-bob/premium
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Defaults are already set to:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
IBM_BOB_BASE_URL="https://api.us-east.bob.ibm.com/inference/v1"
|
|
67
|
+
IBM_BOB_API="openai-completions"
|
|
68
|
+
IBM_BOB_MODELS="premium"
|
|
69
|
+
IBM_BOB_CONTEXT_WINDOW="200000"
|
|
70
|
+
IBM_BOB_MAX_TOKENS="8192"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
For IBM Bob API keys that require `Authorization: Apikey ...` rather than `Authorization: Bearer ...`:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
export IBM_BOB_AUTH_SCHEME="Apikey"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
For SSO, do **not** copy a token out of Bob's local credential store unless IBM policy explicitly permits it. Use `/login ibm-bob` instead.
|
|
80
|
+
|
|
81
|
+
## Configuration
|
|
82
|
+
|
|
83
|
+
### Core
|
|
84
|
+
|
|
85
|
+
| Variable | Default | Description |
|
|
86
|
+
| --- | --- | --- |
|
|
87
|
+
| `IBM_BOB_BASE_URL` | `https://api.us-east.bob.ibm.com/inference/v1` | Approved Bob/IBM endpoint base URL. |
|
|
88
|
+
| `IBM_BOB_MODELS` | `premium` | Comma-separated model IDs exposed by the endpoint. |
|
|
89
|
+
| `IBM_BOB_API_KEY` | unset | Approved API key/token. Keep it out of repo files. |
|
|
90
|
+
| `IBM_BOB_API` | `openai-completions` | One of `openai-completions`, `openai-responses`, `anthropic-messages`. |
|
|
91
|
+
|
|
92
|
+
### Bob routing headers
|
|
93
|
+
|
|
94
|
+
| Variable | Default | Description |
|
|
95
|
+
| --- | --- | --- |
|
|
96
|
+
| `IBM_BOB_READ_BOBSHELL_SETTINGS` | `true` | Read non-secret `instanceId`/`teamId` from `~/.bob/settings.json`. |
|
|
97
|
+
| `IBM_BOB_INSTANCE_ID` | Bob setting | Override `x-instance-id`. |
|
|
98
|
+
| `IBM_BOB_TEAM_ID` | Bob setting | Override `x-team-id`. |
|
|
99
|
+
| `IBM_BOB_USER_AGENT` | `pi-bob/0.1.0` | User-Agent header sent to Bob endpoint. |
|
|
100
|
+
|
|
101
|
+
### Auth headers
|
|
102
|
+
|
|
103
|
+
| Variable | Default | Description |
|
|
104
|
+
| --- | --- | --- |
|
|
105
|
+
| `IBM_BOB_AUTH_SCHEME` | unset/Bearer via Pi's OpenAI adapter | Set to `Apikey` when the Bob token must be sent as `Authorization: Apikey $IBM_BOB_API_KEY`. Do not set this for `/login ibm-bob` SSO. |
|
|
106
|
+
| `IBM_BOB_HEADERS_JSON` | unset | JSON object of extra headers. Values may use Pi env interpolation such as `"$IBM_BOB_API_KEY"`. |
|
|
107
|
+
|
|
108
|
+
### Model metadata
|
|
109
|
+
|
|
110
|
+
| Variable | Default | Description |
|
|
111
|
+
| --- | --- | --- |
|
|
112
|
+
| `IBM_BOB_CONTEXT_WINDOW` | `200000` | Context window Pi should assume. Keep this at or below Bob's backend max input tokens so Pi compacts before Bob rejects the request. |
|
|
113
|
+
| `IBM_BOB_MAX_TOKENS` | `8192` | Max output tokens Pi should request/allow. |
|
|
114
|
+
| `IBM_BOB_INPUT` | `text` | Comma-separated input types: `text` or `text,image`. |
|
|
115
|
+
| `IBM_BOB_REASONING` | `false` | Mark all configured models as reasoning-capable. |
|
|
116
|
+
| `IBM_BOB_REASONING_MODELS` | empty | Comma-separated subset of model IDs that support reasoning. |
|
|
117
|
+
|
|
118
|
+
Pricing is set to zero until IBM-specific metering details are known.
|
|
119
|
+
|
|
120
|
+
### OpenAI compatibility toggles
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
export IBM_BOB_SUPPORTS_DEVELOPER_ROLE=false
|
|
124
|
+
export IBM_BOB_SUPPORTS_REASONING_EFFORT=false
|
|
125
|
+
export IBM_BOB_SUPPORTS_USAGE_IN_STREAMING=true
|
|
126
|
+
export IBM_BOB_SUPPORTS_STRICT_MODE=false
|
|
127
|
+
export IBM_BOB_MAX_TOKENS_FIELD=max_tokens
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Bob's OpenAI-compatible route currently rejects `tools[].function.strict`, so `IBM_BOB_SUPPORTS_STRICT_MODE=false` is the default.
|
|
131
|
+
|
|
132
|
+
## Validation performed
|
|
133
|
+
|
|
134
|
+
Bob Shell SSO itself works locally:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
bob --auth-method sso -m premium -p 'Reply with exactly: bob-ok' --hide-intermediary-output --output-format json
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The response included `bob-ok` and successful usage stats for model `premium`.
|
|
141
|
+
|
|
142
|
+
The unauthenticated Bob model-info endpoint responds as expected when called with a normal User-Agent:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
curl -H 'User-Agent: pi-bob/0.1.0' \
|
|
146
|
+
https://api.us-east.bob.ibm.com/inference/v1/model/info
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Result: HTTP `401` with `Authentication required`, confirming the discovered route exists and requires auth.
|
|
150
|
+
|
|
151
|
+
The Bob SSO endpoint flow was smoke-tested independently: browser SSO callback succeeded, token exchange succeeded, and `GET /inference/v1/model/info` returned HTTP `200` using the fresh SSO token.
|
|
152
|
+
|
|
153
|
+
The Pi provider registers with default settings when an env token is present:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
IBM_BOB_API_KEY=dummy pi -e . --list-models | grep ibm-bob
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Result:
|
|
160
|
+
|
|
161
|
+
```text
|
|
162
|
+
ibm-bob premium 200K 8.2K no no
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
A dummy-token Pi request reaches the Bob endpoint and fails with the expected auth error:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
IBM_BOB_API_KEY=dummy IBM_BOB_AUTH_SCHEME=Apikey \
|
|
169
|
+
pi -e . --model ibm-bob/premium -p 'Say hi'
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Result: HTTP `401 unauthorized`.
|
|
173
|
+
|
|
174
|
+
End-to-end smoke with a fresh Bob SSO token succeeded:
|
|
175
|
+
|
|
176
|
+
```text
|
|
177
|
+
pi -e . --model ibm-bob/premium -p 'Reply with exactly: pi-bob-ok'
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Result:
|
|
181
|
+
|
|
182
|
+
```text
|
|
183
|
+
pi-bob-ok
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Install options
|
|
187
|
+
|
|
188
|
+
Temporary test load:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
pi -e /Users/larry.song/work/hashicorp/pi-bob
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Install as a local Pi package:
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
pi install /Users/larry.song/work/hashicorp/pi-bob
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Remove later:
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
pi remove /Users/larry.song/work/hashicorp/pi-bob
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Next steps
|
|
207
|
+
|
|
208
|
+
Option 1 is wired to the discovered Bob OpenAI-compatible route and `/login ibm-bob` is implemented. Next useful improvements:
|
|
209
|
+
|
|
210
|
+
1. Add a `/bob-status` command that checks auth, model-info, selected instance, and selected team without printing secrets.
|
|
211
|
+
2. Add optional dynamic model discovery from `/inference/v1/model/info` after login.
|
|
212
|
+
3. Add tests around Bob's SSO callback and token refresh helpers.
|
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { OAuthCredentials, OAuthLoginCallbacks } from "@earendil-works/pi-ai";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { createServer, type Server } from "node:http";
|
|
6
|
+
import { createServer as createNetServer } from "node:net";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
|
|
10
|
+
type CompatibleApi = "openai-completions" | "openai-responses" | "anthropic-messages";
|
|
11
|
+
type InputType = "text" | "image";
|
|
12
|
+
type MaxTokensField = "max_tokens" | "max_completion_tokens";
|
|
13
|
+
|
|
14
|
+
interface BobModelConfig {
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
reasoning: boolean;
|
|
18
|
+
input: InputType[];
|
|
19
|
+
contextWindow: number;
|
|
20
|
+
maxTokens: number;
|
|
21
|
+
cost: {
|
|
22
|
+
input: number;
|
|
23
|
+
output: number;
|
|
24
|
+
cacheRead: number;
|
|
25
|
+
cacheWrite: number;
|
|
26
|
+
};
|
|
27
|
+
compat?: Record<string, unknown>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface BobShellSettings {
|
|
31
|
+
ibm?: {
|
|
32
|
+
instanceId?: string;
|
|
33
|
+
teamId?: string;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface BobTokenResponse {
|
|
38
|
+
token: string;
|
|
39
|
+
refresh_token?: string;
|
|
40
|
+
expires_in?: number;
|
|
41
|
+
expires_at?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface CallbackServer {
|
|
45
|
+
port: number;
|
|
46
|
+
callbackUri: string;
|
|
47
|
+
code: Promise<string>;
|
|
48
|
+
close(): void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const PROVIDER_ID = "ibm-bob";
|
|
52
|
+
const DEFAULT_BOB_ORIGIN = "https://api.us-east.bob.ibm.com";
|
|
53
|
+
const DEFAULT_BASE_URL = `${DEFAULT_BOB_ORIGIN}/inference/v1`;
|
|
54
|
+
const DEFAULT_WEB_LOGIN_URL = "https://bob.ibm.com/login";
|
|
55
|
+
const DEFAULT_MODELS = ["premium"];
|
|
56
|
+
const DEFAULT_CONTEXT_WINDOW = 200_000;
|
|
57
|
+
const DEFAULT_MAX_TOKENS = 8192;
|
|
58
|
+
const SUPPORTED_APIS = new Set<CompatibleApi>(["openai-completions", "openai-responses", "anthropic-messages"]);
|
|
59
|
+
|
|
60
|
+
function env(name: string): string | undefined {
|
|
61
|
+
const value = process.env[name]?.trim();
|
|
62
|
+
return value ? value : undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function envInt(name: string, fallback: number): number {
|
|
66
|
+
const value = env(name);
|
|
67
|
+
if (!value) return fallback;
|
|
68
|
+
const parsed = Number.parseInt(value, 10);
|
|
69
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function envBool(name: string, fallback: boolean): boolean {
|
|
73
|
+
const value = env(name);
|
|
74
|
+
if (!value) return fallback;
|
|
75
|
+
if (["1", "true", "yes", "y", "on"].includes(value.toLowerCase())) return true;
|
|
76
|
+
if (["0", "false", "no", "n", "off"].includes(value.toLowerCase())) return false;
|
|
77
|
+
return fallback;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function envCsv(name: string, fallback: string[] = []): string[] {
|
|
81
|
+
const value = env(name);
|
|
82
|
+
if (!value) return fallback;
|
|
83
|
+
return value
|
|
84
|
+
.split(",")
|
|
85
|
+
.map((item) => item.trim())
|
|
86
|
+
.filter(Boolean);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function readBobShellSettings(): BobShellSettings | undefined {
|
|
90
|
+
if (!envBool("IBM_BOB_READ_BOBSHELL_SETTINGS", true)) return undefined;
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const raw = readFileSync(join(homedir(), ".bob", "settings.json"), "utf8");
|
|
94
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
95
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
|
|
96
|
+
|
|
97
|
+
const ibm = (parsed as BobShellSettings).ibm;
|
|
98
|
+
return {
|
|
99
|
+
ibm: {
|
|
100
|
+
instanceId: typeof ibm?.instanceId === "string" ? ibm.instanceId : undefined,
|
|
101
|
+
teamId: typeof ibm?.teamId === "string" ? ibm.teamId : undefined,
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
} catch {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function providerBaseUrl(): string {
|
|
110
|
+
return env("IBM_BOB_BASE_URL") ?? DEFAULT_BASE_URL;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function bobOrigin(): string {
|
|
114
|
+
const explicit = env("IBM_BOB_AUTH_BASE_URL");
|
|
115
|
+
if (explicit) return explicit.replace(/\/$/, "");
|
|
116
|
+
try {
|
|
117
|
+
return new URL(providerBaseUrl()).origin;
|
|
118
|
+
} catch {
|
|
119
|
+
return DEFAULT_BOB_ORIGIN;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function bobWebLoginUrl(): string {
|
|
124
|
+
if (env("IBM_BOB_WEB_LOGIN_URL")) return env("IBM_BOB_WEB_LOGIN_URL")!;
|
|
125
|
+
try {
|
|
126
|
+
const host = new URL(bobOrigin()).host;
|
|
127
|
+
if (host === "api.dev.bob.ibm.com") return "https://public-dev.bob.ibm.com/login";
|
|
128
|
+
if (host === "api.qa-test.bob.ibm.com") return "https://qa.bob.ibm.com/login";
|
|
129
|
+
return DEFAULT_WEB_LOGIN_URL;
|
|
130
|
+
} catch {
|
|
131
|
+
return DEFAULT_WEB_LOGIN_URL;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function parseApi(): CompatibleApi {
|
|
136
|
+
const requested = env("IBM_BOB_API") ?? "openai-completions";
|
|
137
|
+
if (SUPPORTED_APIS.has(requested as CompatibleApi)) return requested as CompatibleApi;
|
|
138
|
+
console.warn(
|
|
139
|
+
`pi-bob: unsupported IBM_BOB_API=${JSON.stringify(requested)}; falling back to openai-completions.`,
|
|
140
|
+
);
|
|
141
|
+
return "openai-completions";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function parseInputTypes(): InputType[] {
|
|
145
|
+
const values = envCsv("IBM_BOB_INPUT", ["text"]);
|
|
146
|
+
const input = values.filter((value): value is InputType => value === "text" || value === "image");
|
|
147
|
+
return input.length > 0 ? [...new Set(input)] : ["text"];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function parseJsonHeaders(): Record<string, string> | undefined {
|
|
151
|
+
const json = env("IBM_BOB_HEADERS_JSON");
|
|
152
|
+
if (!json) return undefined;
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const parsed = JSON.parse(json) as unknown;
|
|
156
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
157
|
+
console.warn("pi-bob: IBM_BOB_HEADERS_JSON must be a JSON object; ignoring it.");
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const headers: Record<string, string> = {};
|
|
162
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
163
|
+
if (typeof value === "string") headers[key] = value;
|
|
164
|
+
else console.warn(`pi-bob: header ${JSON.stringify(key)} is not a string; ignoring it.`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return Object.keys(headers).length > 0 ? headers : undefined;
|
|
168
|
+
} catch (error) {
|
|
169
|
+
console.warn(
|
|
170
|
+
`pi-bob: failed to parse IBM_BOB_HEADERS_JSON; ignoring it. ${
|
|
171
|
+
error instanceof Error ? error.message : String(error)
|
|
172
|
+
}`,
|
|
173
|
+
);
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function buildHeaders(settings?: BobShellSettings): Record<string, string> | undefined {
|
|
179
|
+
const headers: Record<string, string> = {
|
|
180
|
+
"User-Agent": env("IBM_BOB_USER_AGENT") ?? "pi-bob/0.1.0",
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const instanceId = env("IBM_BOB_INSTANCE_ID") ?? settings?.ibm?.instanceId;
|
|
184
|
+
const teamId = env("IBM_BOB_TEAM_ID") ?? settings?.ibm?.teamId;
|
|
185
|
+
if (instanceId) headers["x-instance-id"] = instanceId;
|
|
186
|
+
if (teamId) headers["x-team-id"] = teamId;
|
|
187
|
+
|
|
188
|
+
const authScheme = env("IBM_BOB_AUTH_SCHEME");
|
|
189
|
+
if (authScheme && authScheme.toLowerCase() !== "bearer") {
|
|
190
|
+
headers.Authorization = `${authScheme} $IBM_BOB_API_KEY`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const jsonHeaders = parseJsonHeaders();
|
|
194
|
+
if (jsonHeaders) Object.assign(headers, jsonHeaders);
|
|
195
|
+
|
|
196
|
+
return Object.keys(headers).length > 0 ? headers : undefined;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function modelName(id: string): string {
|
|
200
|
+
const known: Record<string, string> = {
|
|
201
|
+
premium: "IBM Bob Premium",
|
|
202
|
+
pro: "IBM Bob Pro",
|
|
203
|
+
flash: "IBM Bob Flash",
|
|
204
|
+
"flash-lite": "IBM Bob Flash Lite",
|
|
205
|
+
"bob-3-pro-preview": "IBM Bob 3 Pro Preview",
|
|
206
|
+
};
|
|
207
|
+
if (known[id]) return known[id];
|
|
208
|
+
|
|
209
|
+
return id
|
|
210
|
+
.split(/[/:._-]+/)
|
|
211
|
+
.filter(Boolean)
|
|
212
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
213
|
+
.join(" ");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function parseReasoningModels(): Set<string> {
|
|
217
|
+
return new Set(envCsv("IBM_BOB_REASONING_MODELS"));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function buildOpenAiCompat(api: CompatibleApi): Record<string, unknown> | undefined {
|
|
221
|
+
if (api === "anthropic-messages") return undefined;
|
|
222
|
+
|
|
223
|
+
const compat: Record<string, unknown> = {
|
|
224
|
+
supportsDeveloperRole: envBool("IBM_BOB_SUPPORTS_DEVELOPER_ROLE", false),
|
|
225
|
+
supportsReasoningEffort: envBool("IBM_BOB_SUPPORTS_REASONING_EFFORT", false),
|
|
226
|
+
supportsUsageInStreaming: envBool("IBM_BOB_SUPPORTS_USAGE_IN_STREAMING", true),
|
|
227
|
+
supportsStrictMode: envBool("IBM_BOB_SUPPORTS_STRICT_MODE", false),
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const maxTokensField = env("IBM_BOB_MAX_TOKENS_FIELD") as MaxTokensField | undefined;
|
|
231
|
+
if (maxTokensField === "max_tokens" || maxTokensField === "max_completion_tokens") {
|
|
232
|
+
compat.maxTokensField = maxTokensField;
|
|
233
|
+
} else {
|
|
234
|
+
compat.maxTokensField = api === "openai-completions" ? "max_tokens" : "max_completion_tokens";
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return compat;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function buildAnthropicCompat(api: CompatibleApi): Record<string, unknown> | undefined {
|
|
241
|
+
if (api !== "anthropic-messages") return undefined;
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
supportsEagerToolInputStreaming: envBool("IBM_BOB_SUPPORTS_EAGER_TOOL_INPUT_STREAMING", false),
|
|
245
|
+
supportsLongCacheRetention: envBool("IBM_BOB_SUPPORTS_LONG_CACHE_RETENTION", true),
|
|
246
|
+
supportsCacheControlOnTools: envBool("IBM_BOB_SUPPORTS_CACHE_CONTROL_ON_TOOLS", true),
|
|
247
|
+
forceAdaptiveThinking: envBool("IBM_BOB_FORCE_ADAPTIVE_THINKING", false),
|
|
248
|
+
allowEmptySignature: envBool("IBM_BOB_ALLOW_EMPTY_SIGNATURE", false),
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function buildModels(api: CompatibleApi): BobModelConfig[] {
|
|
253
|
+
const ids = envCsv("IBM_BOB_MODELS", DEFAULT_MODELS);
|
|
254
|
+
const reasoningModels = parseReasoningModels();
|
|
255
|
+
const allReasoning = envBool("IBM_BOB_REASONING", false);
|
|
256
|
+
const input = parseInputTypes();
|
|
257
|
+
const contextWindow = envInt("IBM_BOB_CONTEXT_WINDOW", DEFAULT_CONTEXT_WINDOW);
|
|
258
|
+
const maxTokens = envInt("IBM_BOB_MAX_TOKENS", DEFAULT_MAX_TOKENS);
|
|
259
|
+
const compat = buildOpenAiCompat(api) ?? buildAnthropicCompat(api);
|
|
260
|
+
|
|
261
|
+
return ids.map((id) => ({
|
|
262
|
+
id,
|
|
263
|
+
name: modelName(id),
|
|
264
|
+
reasoning: allReasoning || reasoningModels.has(id),
|
|
265
|
+
input,
|
|
266
|
+
contextWindow,
|
|
267
|
+
maxTokens,
|
|
268
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
269
|
+
...(compat ? { compat } : {}),
|
|
270
|
+
}));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function jwtExpiry(accessToken: string): number | undefined {
|
|
274
|
+
try {
|
|
275
|
+
const [, payload] = accessToken.split(".");
|
|
276
|
+
if (!payload) return undefined;
|
|
277
|
+
const json = Buffer.from(payload.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8");
|
|
278
|
+
const parsed = JSON.parse(json) as { exp?: unknown };
|
|
279
|
+
if (typeof parsed.exp !== "number") return undefined;
|
|
280
|
+
return parsed.exp * 1000 - 5 * 60 * 1000;
|
|
281
|
+
} catch {
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function credentialsFromTokenResponse(response: BobTokenResponse, previousRefresh?: string): OAuthCredentials {
|
|
287
|
+
if (!response.token) throw new Error("IBM Bob SSO response did not include an access token.");
|
|
288
|
+
return {
|
|
289
|
+
access: response.token,
|
|
290
|
+
refresh: response.refresh_token ?? previousRefresh ?? "",
|
|
291
|
+
expires:
|
|
292
|
+
jwtExpiry(response.token) ??
|
|
293
|
+
(typeof response.expires_at === "number"
|
|
294
|
+
? response.expires_at - 5 * 60 * 1000
|
|
295
|
+
: Date.now() + (response.expires_in ?? 55 * 60) * 1000 - 5 * 60 * 1000),
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function choosePort(): Promise<number> {
|
|
300
|
+
const configured = envInt("SSO_PORT", 0) || envInt("IBM_BOB_SSO_PORT", 0);
|
|
301
|
+
if (configured) return configured;
|
|
302
|
+
|
|
303
|
+
return new Promise((resolve, reject) => {
|
|
304
|
+
const server = createNetServer();
|
|
305
|
+
server.on("error", reject);
|
|
306
|
+
server.listen(0, () => {
|
|
307
|
+
const address = server.address();
|
|
308
|
+
const port = typeof address === "object" && address ? address.port : undefined;
|
|
309
|
+
server.close(() => (port ? resolve(port) : reject(new Error("Failed to allocate callback port."))));
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async function startCallbackServer(state: string): Promise<CallbackServer> {
|
|
315
|
+
const port = await choosePort();
|
|
316
|
+
const server = createServer();
|
|
317
|
+
|
|
318
|
+
const code = new Promise<string>((resolve, reject) => {
|
|
319
|
+
server.on("request", (req, res) => {
|
|
320
|
+
if (!req.url?.startsWith("/bob-callback")) {
|
|
321
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
322
|
+
res.end("Not Found");
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
328
|
+
const returnedState = url.searchParams.get("state");
|
|
329
|
+
const authCode = url.searchParams.get("code");
|
|
330
|
+
const error = url.searchParams.get("error");
|
|
331
|
+
|
|
332
|
+
if (returnedState !== state) {
|
|
333
|
+
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
334
|
+
res.end("Invalid state parameter");
|
|
335
|
+
reject(new Error("Invalid state parameter from IBM Bob SSO callback."));
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (error) {
|
|
340
|
+
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
341
|
+
res.end("Authentication failed");
|
|
342
|
+
reject(new Error(`IBM Bob SSO failed: ${error}`));
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (!authCode) {
|
|
347
|
+
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
348
|
+
res.end("Missing authorization code");
|
|
349
|
+
reject(new Error("IBM Bob SSO callback did not include an authorization code."));
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
354
|
+
res.end(`<!doctype html>
|
|
355
|
+
<html><body style="font-family: system-ui; margin: 3rem;">
|
|
356
|
+
<h1>IBM Bob authentication successful</h1>
|
|
357
|
+
<p>You can close this window and return to Pi.</p>
|
|
358
|
+
<script>setTimeout(() => window.close(), 1000);</script>
|
|
359
|
+
</body></html>`);
|
|
360
|
+
resolve(authCode);
|
|
361
|
+
setTimeout(() => server.close(), 500);
|
|
362
|
+
} catch (error) {
|
|
363
|
+
reject(error);
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
server.on("error", reject);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
await new Promise<void>((resolve, reject) => {
|
|
370
|
+
server.once("error", reject);
|
|
371
|
+
server.listen(port, () => {
|
|
372
|
+
server.off("error", reject);
|
|
373
|
+
resolve();
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
return {
|
|
378
|
+
port,
|
|
379
|
+
callbackUri: `http://localhost:${port}/bob-callback`,
|
|
380
|
+
code,
|
|
381
|
+
close() {
|
|
382
|
+
server.close();
|
|
383
|
+
},
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async function postToken(path: "/authn/v1/auth/token" | "/authn/v1/auth/refresh", body: unknown): Promise<BobTokenResponse> {
|
|
388
|
+
const response = await fetch(`${bobOrigin()}${path}`, {
|
|
389
|
+
method: "POST",
|
|
390
|
+
headers: { "Content-Type": "application/json", "User-Agent": env("IBM_BOB_USER_AGENT") ?? "pi-bob/0.1.0" },
|
|
391
|
+
body: JSON.stringify(body),
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
if (!response.ok) {
|
|
395
|
+
throw new Error(`IBM Bob SSO token request failed: ${response.status} ${await response.text()}`);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return (await response.json()) as BobTokenResponse;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async function loginBob(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
|
|
402
|
+
const state = randomBytes(16).toString("hex");
|
|
403
|
+
const callbackServer = await startCallbackServer(state);
|
|
404
|
+
const loginUrl = new URL(bobWebLoginUrl());
|
|
405
|
+
loginUrl.searchParams.set("callback_uri", callbackServer.callbackUri);
|
|
406
|
+
loginUrl.searchParams.set("state", state);
|
|
407
|
+
|
|
408
|
+
try {
|
|
409
|
+
callbacks.onAuth({ url: loginUrl.toString() });
|
|
410
|
+
callbacks.onDeviceCode?.({
|
|
411
|
+
userCode: "Browser SSO",
|
|
412
|
+
verificationUri: loginUrl.toString(),
|
|
413
|
+
expiresInSeconds: Math.floor(envInt("IBM_BOB_LOGIN_TIMEOUT_MS", 180_000) / 1000),
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
const timeoutMs = envInt("IBM_BOB_LOGIN_TIMEOUT_MS", 180_000);
|
|
417
|
+
const timeout = new Promise<never>((_resolve, reject) => {
|
|
418
|
+
setTimeout(() => reject(new Error("IBM Bob SSO timed out.")), timeoutMs);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
const code = await Promise.race([callbackServer.code, timeout]);
|
|
422
|
+
return credentialsFromTokenResponse(await postToken("/authn/v1/auth/token", { code }));
|
|
423
|
+
} finally {
|
|
424
|
+
callbackServer.close();
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async function refreshBobToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {
|
|
429
|
+
if (!credentials.refresh) throw new Error("No IBM Bob refresh token is available. Run /login ibm-bob again.");
|
|
430
|
+
return credentialsFromTokenResponse(
|
|
431
|
+
await postToken("/authn/v1/auth/refresh", { refresh_token: credentials.refresh }),
|
|
432
|
+
credentials.refresh,
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export default function (pi: ExtensionAPI) {
|
|
437
|
+
const api = parseApi();
|
|
438
|
+
const settings = readBobShellSettings();
|
|
439
|
+
const headers = buildHeaders(settings);
|
|
440
|
+
|
|
441
|
+
pi.registerProvider(PROVIDER_ID, {
|
|
442
|
+
name: "IBM Bob",
|
|
443
|
+
baseUrl: providerBaseUrl(),
|
|
444
|
+
apiKey: "$IBM_BOB_API_KEY",
|
|
445
|
+
api,
|
|
446
|
+
...(headers ? { headers } : {}),
|
|
447
|
+
models: buildModels(api),
|
|
448
|
+
oauth: {
|
|
449
|
+
name: "IBM Bob SSO",
|
|
450
|
+
login: loginBob,
|
|
451
|
+
refreshToken: refreshBobToken,
|
|
452
|
+
getApiKey: (credentials) => credentials.access,
|
|
453
|
+
},
|
|
454
|
+
});
|
|
455
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-bob",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Pi provider extension for IBM Bob / IBM-approved compatible model endpoints.",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"pi-package",
|
|
9
|
+
"pi-coding-agent",
|
|
10
|
+
"ibm",
|
|
11
|
+
"bob",
|
|
12
|
+
"llm-provider"
|
|
13
|
+
],
|
|
14
|
+
"pi": {
|
|
15
|
+
"extensions": [
|
|
16
|
+
"./extensions/bob.ts"
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
"author": "Larry Song",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "https://github.com/songlining/pi-bob.git"
|
|
24
|
+
},
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/songlining/pi-bob/issues"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/songlining/pi-bob#readme",
|
|
29
|
+
"scripts": {
|
|
30
|
+
"check": "pi -e . --list-models >/dev/null",
|
|
31
|
+
"clean": "echo 'nothing to clean'",
|
|
32
|
+
"build": "echo 'nothing to build'"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@earendil-works/pi-coding-agent": "*"
|
|
36
|
+
}
|
|
37
|
+
}
|