sfbench 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/LICENSE +21 -0
- package/README.ja.md +165 -0
- package/README.md +164 -0
- package/dist/server/ai/agent.js +132 -0
- package/dist/server/ai/agentTasks.js +296 -0
- package/dist/server/ai/anthropic.js +36 -0
- package/dist/server/ai/conversation.js +3 -0
- package/dist/server/ai/errors.js +75 -0
- package/dist/server/ai/openai.js +53 -0
- package/dist/server/ai/tools.js +249 -0
- package/dist/server/browser.js +26 -0
- package/dist/server/cli.js +151 -0
- package/dist/server/fsutil.js +17 -0
- package/dist/server/http/security.js +37 -0
- package/dist/server/http/server.js +778 -0
- package/dist/server/launcher.js +170 -0
- package/dist/server/log.js +33 -0
- package/dist/server/orgs.js +143 -0
- package/dist/server/paths.js +35 -0
- package/dist/server/records.js +118 -0
- package/dist/server/rest.js +120 -0
- package/dist/server/salesforce.js +348 -0
- package/dist/server/serve.js +121 -0
- package/dist/server/sf/apexRun.js +112 -0
- package/dist/server/sf/apiRequest.js +138 -0
- package/dist/server/sf/commands.js +143 -0
- package/dist/server/sf/errors.js +38 -0
- package/dist/server/sf/run.js +163 -0
- package/dist/server/state.js +103 -0
- package/dist/server/store.js +201 -0
- package/dist/server/version.js +8 -0
- package/dist/shared/api.js +31 -0
- package/dist/shared/namespace.js +28 -0
- package/dist/web/assets/index-CW8D9pEg.js +210 -0
- package/dist/web/assets/index-LqgSbazj.css +1 -0
- package/dist/web/favicon.svg +1 -0
- package/dist/web/index.html +14 -0
- package/dist/web/licenses.txt +1434 -0
- package/package.json +78 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Atsuhiko Kimura
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.ja.md
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# sfbench
|
|
2
|
+
|
|
3
|
+
sfbench は、Salesforce の SOQL、REST API、匿名 Apex を手元のブラウザで実行するツールです。
|
|
4
|
+
認証は sf CLI に保存済みのものを使うので、接続アプリケーションの作成や組織の設定は要りません。
|
|
5
|
+
|
|
6
|
+
[English README](README.md)
|
|
7
|
+
|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
sfbench は [lwc-soql-builder](https://github.com/lwc-soql-builder/lwc-soql-builder) の後継です。
|
|
11
|
+
lwc-soql-builder は、Salesforce が OAuth のユーザーエージェントフローを使えなくしたため、ログインできなくなりました。
|
|
12
|
+
|
|
13
|
+
## 特徴
|
|
14
|
+
|
|
15
|
+
### SOQL
|
|
16
|
+
|
|
17
|
+
- クリック操作で SOQL を組み立てる。項目を API 名でもラベルでも探せる
|
|
18
|
+
- 親リレーション、子リレーション(サブクエリ)をたどって項目を選ぶ
|
|
19
|
+
- カーソルの位置に応じた補完(FROM のあとはオブジェクト、`Account.` のあとは参照先の項目、サブクエリの中は子オブジェクトの項目)
|
|
20
|
+
- 管理パッケージの名前空間を省略して書ける
|
|
21
|
+
- 数万件の結果も、スクロールに合わせて続きを読み込む。子リレーションのレコードは表で開く
|
|
22
|
+
- 読み込んだ結果の行を絞り込み、全件を CSV に出力する
|
|
23
|
+
- 結果の表で値を書き換えて保存し、行を選んで削除する
|
|
24
|
+
- Tooling API と Query All でのクエリ、API バージョンの指定
|
|
25
|
+
|
|
26
|
+
### REST API
|
|
27
|
+
|
|
28
|
+
- メソッド、パス、ヘッダー、JSON のボディを入れて送る。ボディの整形と色分け
|
|
29
|
+
- 4xx と 5xx もレスポンスとして、ステータス、ヘッダー、本文を見る
|
|
30
|
+
- 送った要求は実行履歴に残り、押すと欄に読み込む
|
|
31
|
+
|
|
32
|
+
### 匿名 Apex
|
|
33
|
+
|
|
34
|
+
- Apex を色分けして書き、実行する。API バージョンも指定できる
|
|
35
|
+
- コンパイルエラーと例外は、行と列へ移るボタン付きで出す
|
|
36
|
+
- デバッグログを検索し、`USER_DEBUG` の行だけに絞る
|
|
37
|
+
- 実行したコードは実行履歴に残る
|
|
38
|
+
|
|
39
|
+
### AI
|
|
40
|
+
|
|
41
|
+
- やりたいことを書くと、AI が SOQL、REST API の要求、匿名 Apex を組む
|
|
42
|
+
- 失敗したクエリ、要求、Apex を、エラー文をもとに AI が直す
|
|
43
|
+
- AI の提案は差分で見せ、置き換えるまで画面を変えない。REST API と Apex は、置き換えても自動では送らない・実行しない
|
|
44
|
+
- Anthropic か OpenAI の API キーを、設定画面で入れて使う
|
|
45
|
+
|
|
46
|
+
### そのほか
|
|
47
|
+
|
|
48
|
+
- AI のエージェント(Claude Code など)から、クエリや要求を入れた状態で起動できる
|
|
49
|
+
- 本番の組織では、データを変える操作の前に確認する
|
|
50
|
+
- 結果が分からなかった要求(タイムアウトなど)を送り直す前に確認する
|
|
51
|
+
|
|
52
|
+
## 必要なもの
|
|
53
|
+
|
|
54
|
+
- Node.js 22.21.0 以上の 22 系、または 24.5.0 以上
|
|
55
|
+
- [Salesforce CLI](https://developer.salesforce.com/tools/salesforcecli)(`sf`)と、使う組織へのログイン
|
|
56
|
+
|
|
57
|
+
macOS、Linux、Windows で動きます。
|
|
58
|
+
組織へのログインは sf CLI で行います。
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
sf org login web -a my-org
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## 使い方
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
npx sfbench
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
ブラウザで sfbench が開きます。
|
|
71
|
+
組織は、画面右上の組織の切り替えから選びます。
|
|
72
|
+
`-o` を付けない場合は、sf CLI の既定の組織(`sf config set target-org <alias>` で設定したもの)で開きます。
|
|
73
|
+
|
|
74
|
+
画面の上のタブで、SOQL、REST API、Apex を切り替えます。
|
|
75
|
+
タブを切り替えても、それぞれの画面の入力や結果は残ります。
|
|
76
|
+
各画面の使い方は、[ヘルプ](help/ja/README.md)にあります。
|
|
77
|
+
|
|
78
|
+
### コマンドライン引数
|
|
79
|
+
|
|
80
|
+
| 引数 | 内容 |
|
|
81
|
+
| --- | --- |
|
|
82
|
+
| `-o, --target-org <alias>` | 開く組織 |
|
|
83
|
+
| `--query "<SOQL>"` | SOQL を入力した状態で開く |
|
|
84
|
+
| `--run` | `--query` の SOQL を実行した状態で開く |
|
|
85
|
+
| `--rest <path>` | REST API の画面を、この要求を入れた状態で開く。送信はしない |
|
|
86
|
+
| `--method <method>` | `--rest` の要求のメソッド。GET(既定)、POST、PATCH、PUT、DELETE |
|
|
87
|
+
| `--body <json>`、`--body-file <file>` | `--rest` の要求の JSON のボディ |
|
|
88
|
+
| `--apex "<code>"`、`--apex-file <file>` | Apex の画面を、このコードを入れた状態で開く。実行はしない |
|
|
89
|
+
| `--json` | 開いた URL などを JSON で出力する |
|
|
90
|
+
| `--no-open` | ブラウザを開かず、URL だけを出力する |
|
|
91
|
+
| `stop` | 起動中の sfbench を止める |
|
|
92
|
+
|
|
93
|
+
データを変えうる REST API の要求と Apex は、入れた状態で開くだけで、送信や実行は画面のボタンで行います。
|
|
94
|
+
|
|
95
|
+
### AI のエージェントから使う
|
|
96
|
+
|
|
97
|
+
AI のエージェントは、sfbench をまだ知りません。
|
|
98
|
+
何も教えずに「sfbench で〜」と頼むと、Claude Code は `sfbench` コマンドを探して見つけられず、sf CLI で直接クエリを実行しました。
|
|
99
|
+
Claude Code には、sfbench の使い方を教えるスキルを、このリポジトリからプラグインとして入れられます。
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
claude plugin install sfbench --marketplace atskimura/sfbench
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Claude Code の中では `/plugin install sfbench --marketplace atskimura/sfbench` でも入れられます。
|
|
106
|
+
指示ファイル(`CLAUDE.md`)を書き換える必要はありません。
|
|
107
|
+
スキルのファイルを自分で置くこともできます。
|
|
108
|
+
日本語版は [plugins/sfbench/ja/sfbench/SKILL.md](plugins/sfbench/ja/sfbench/SKILL.md)、英語版は [plugins/sfbench/skills/sfbench/SKILL.md](plugins/sfbench/skills/sfbench/SKILL.md) です。
|
|
109
|
+
`sfbench` フォルダごと、エージェントがスキルを読む場所(Claude Code なら `~/.claude/skills/`、Codex なら `~/.agents/skills/`)に置きます(Codex では試していません)。
|
|
110
|
+
|
|
111
|
+
スキルを入れた Claude Code に「sfbench で成約済みの商談を一覧して」と頼むと、エージェントは SOQL を組み立てて次のように起動します。
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
npx -y sfbench -o my-org --query "SELECT Id, Name, Amount FROM Opportunity WHERE IsWon = true" --run
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
sfbench はブラウザでクエリを実行した結果を開き、コマンドはすぐ終了します。
|
|
118
|
+
すでに起動している場合は、新しいタブを開きます。
|
|
119
|
+
|
|
120
|
+
REST API の要求や Apex も、同じように入れた状態で開けます。
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
npx -y sfbench -o my-org --rest /services/data/v62.0/sobjects/Account --method POST --body-file account.json
|
|
124
|
+
npx -y sfbench -o my-org --apex-file fix-data.apex
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### AI で組む機能
|
|
128
|
+
|
|
129
|
+
設定画面(右上の歯車)で、AI の提供元(Anthropic か OpenAI)、モデル ID、API キーを入れます。
|
|
130
|
+
API キーはこの PC の `~/.sfbench/settings.json` に本人だけが読める形で保存し、ブラウザには表示しません。
|
|
131
|
+
AI を呼ぶのは「生成」か「AI で直す」を押したときだけで、呼ぶたびに API キーの持ち主に提供元の料金がかかります。
|
|
132
|
+
|
|
133
|
+
AI に送るのは、オブジェクトと項目の名前、型、ラベル、選択リストの値、画面の内容(エディタのクエリやコード、いまの要求)、直すときのエラー文です。
|
|
134
|
+
送る情報は入力欄の下の札で示し、画面の内容は札を押せば送らないようにできます。
|
|
135
|
+
SOQL の結果は送りません。
|
|
136
|
+
REST API のレスポンスと Apex の実行結果は、「この結果を使って AI に頼む」を押したときだけ送ります。
|
|
137
|
+
|
|
138
|
+
## 仕組み
|
|
139
|
+
|
|
140
|
+
sfbench は手元で小さな Web サーバー(`http://127.0.0.1:7717`。使用中なら 7736 までの空いているポート)を起動し、ブラウザで画面を開きます。
|
|
141
|
+
Salesforce への問い合わせはこのサーバーが行い、アクセストークンは sf CLI から取得します。
|
|
142
|
+
トークンがブラウザやほかのサーバーに渡ることはありません。
|
|
143
|
+
サーバーは自動では止まりません。止めるときは `npx sfbench stop` を実行します。
|
|
144
|
+
|
|
145
|
+
設定、最近のクエリ、REST API と Apex の実行履歴は `~/.sfbench/` に保存します。
|
|
146
|
+
サーバーのログは `~/.sfbench/server.log` にあります。
|
|
147
|
+
ログには、SOQL、要求のボディ、Apex のコード、レコードの値を書きません。
|
|
148
|
+
|
|
149
|
+
## 開発
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
npm install
|
|
153
|
+
npm run dev # 画面の開発サーバーとローカルサーバーを起動する
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
`http://localhost:5173/launch/dev` を開きます。
|
|
157
|
+
コードを保存すると、画面とサーバーにそのまま反映されます。
|
|
158
|
+
|
|
159
|
+
設計は [docs/design.md](docs/design.md) にあります。
|
|
160
|
+
|
|
161
|
+
## ライセンス
|
|
162
|
+
|
|
163
|
+
MIT
|
|
164
|
+
|
|
165
|
+
画面に取り込んでいるライブラリのライセンスは、設定画面の「ライセンス」から見られます(公開物の `dist/web/licenses.txt`)。
|
package/README.md
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# sfbench
|
|
2
|
+
|
|
3
|
+
sfbench runs Salesforce SOQL, REST API requests and anonymous Apex in your browser, on your own machine.
|
|
4
|
+
It uses the auth you already have in the sf CLI, so there is no connected app to create and nothing to set up in your org.
|
|
5
|
+
|
|
6
|
+
[日本語の README](README.ja.md)
|
|
7
|
+
|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
sfbench is the successor to [lwc-soql-builder](https://github.com/lwc-soql-builder/lwc-soql-builder).
|
|
11
|
+
lwc-soql-builder can no longer log in, because Salesforce has blocked the OAuth user-agent flow it relied on.
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
### SOQL
|
|
16
|
+
|
|
17
|
+
- Build SOQL by clicking. Find fields by API name or by label
|
|
18
|
+
- Pick fields through parent relationships and child relationships (subqueries)
|
|
19
|
+
- Completion that follows the cursor: objects after FROM, the target's fields after `Account.`, the child object's fields inside a subquery
|
|
20
|
+
- Leave out the namespace of a managed package
|
|
21
|
+
- Results of tens of thousands of rows load as you scroll. Child relationship records open in their own table
|
|
22
|
+
- Filter the loaded rows, and export the whole result to CSV
|
|
23
|
+
- Edit values in the result table and save them, or select rows and delete them
|
|
24
|
+
- Queries with the Tooling API and Query All, on the API version you choose
|
|
25
|
+
|
|
26
|
+
### REST API
|
|
27
|
+
|
|
28
|
+
- Send a request with its method, path, headers and JSON body. The body is formatted and colored
|
|
29
|
+
- See 4xx and 5xx answers as responses, with their status, headers and body
|
|
30
|
+
- Requests you sent stay in the history; click one to load it again
|
|
31
|
+
|
|
32
|
+
### Anonymous Apex
|
|
33
|
+
|
|
34
|
+
- Write Apex with syntax colors and run it, on the API version you choose
|
|
35
|
+
- Compile errors and exceptions come with a button that goes to their line and column
|
|
36
|
+
- Search the debug log, or show only its `USER_DEBUG` lines
|
|
37
|
+
- The code you ran stays in the history
|
|
38
|
+
|
|
39
|
+
### AI
|
|
40
|
+
|
|
41
|
+
- Write what you want, and an AI writes the SOQL, the REST API request or the anonymous Apex
|
|
42
|
+
- An AI fixes a query, a request or Apex that failed, from its error
|
|
43
|
+
- Proposals show as differences and change nothing until you replace. REST API requests and Apex are never sent or run by the AI: you press Send or Run
|
|
44
|
+
- Bring your own Anthropic or OpenAI API key, set in the settings
|
|
45
|
+
|
|
46
|
+
### And more
|
|
47
|
+
|
|
48
|
+
- AI agents (such as Claude Code) can open sfbench with a query, a request or Apex filled in
|
|
49
|
+
- In a production org, sfbench asks before anything that changes data
|
|
50
|
+
- sfbench asks before sending again a request whose outcome is not known (after a timeout, for example)
|
|
51
|
+
|
|
52
|
+
## Requirements
|
|
53
|
+
|
|
54
|
+
- Node.js 22.21.0 or later in the 22 line, or 24.5.0 or later
|
|
55
|
+
- The [Salesforce CLI](https://developer.salesforce.com/tools/salesforcecli) (`sf`), logged in to the orgs you use
|
|
56
|
+
|
|
57
|
+
sfbench runs on macOS, Linux and Windows.
|
|
58
|
+
You log in to orgs with the sf CLI:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
sf org login web -a my-org
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Usage
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
npx sfbench
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
sfbench opens in your browser.
|
|
71
|
+
Choose an org from the org switcher at the top right.
|
|
72
|
+
Without `-o`, sfbench opens the sf CLI's default org (the one set with `sf config set target-org <alias>`).
|
|
73
|
+
|
|
74
|
+
The tabs at the top switch between SOQL, REST API and Apex.
|
|
75
|
+
Each screen keeps what you typed and its result while another is shown.
|
|
76
|
+
The [help](help/en/README.md) explains each screen.
|
|
77
|
+
|
|
78
|
+
### Command-line options
|
|
79
|
+
|
|
80
|
+
| Option | Description |
|
|
81
|
+
| --- | --- |
|
|
82
|
+
| `-o, --target-org <alias>` | The org to open |
|
|
83
|
+
| `--query "<SOQL>"` | Open with this SOQL in the editor |
|
|
84
|
+
| `--run` | Also run the `--query` SOQL |
|
|
85
|
+
| `--rest <path>` | Open the REST API screen with this request. It is not sent |
|
|
86
|
+
| `--method <method>` | The `--rest` request's method: GET (default), POST, PATCH, PUT or DELETE |
|
|
87
|
+
| `--body <json>`, `--body-file <file>` | The `--rest` request's JSON body |
|
|
88
|
+
| `--apex "<code>"`, `--apex-file <file>` | Open the Apex screen with this code. It is not run |
|
|
89
|
+
| `--json` | Print the opened URL and more as JSON |
|
|
90
|
+
| `--no-open` | Print the URL without opening a browser |
|
|
91
|
+
| `stop` | Stop the running sfbench |
|
|
92
|
+
|
|
93
|
+
A REST API request or Apex, which may change data, is only filled in: you send or run it with the button on the screen.
|
|
94
|
+
|
|
95
|
+
### From an AI agent
|
|
96
|
+
|
|
97
|
+
AI agents do not know sfbench yet.
|
|
98
|
+
Asked to "use sfbench" with no hint, Claude Code looked for an `sfbench` command, did not find it, and ran the query with the sf CLI instead.
|
|
99
|
+
For Claude Code, install the skill that teaches it sfbench, as a plugin from this repository:
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
claude plugin install sfbench --marketplace atskimura/sfbench
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Inside Claude Code, `/plugin install sfbench --marketplace atskimura/sfbench` does the same.
|
|
106
|
+
Your instructions file (`CLAUDE.md`) stays as it is.
|
|
107
|
+
You can also put the skill's file in place yourself: [plugins/sfbench/skills/sfbench/SKILL.md](plugins/sfbench/skills/sfbench/SKILL.md) in English, or [plugins/sfbench/ja/sfbench/SKILL.md](plugins/sfbench/ja/sfbench/SKILL.md) in Japanese.
|
|
108
|
+
Copy the `sfbench` folder to where the agent reads skills (`~/.claude/skills/` for Claude Code, `~/.agents/skills/` for Codex; untested with Codex).
|
|
109
|
+
|
|
110
|
+
With the skill, ask Claude Code to "list the won opportunities in sfbench", and it writes the SOQL and starts sfbench like this:
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
npx -y sfbench -o my-org --query "SELECT Id, Name, Amount FROM Opportunity WHERE IsWon = true" --run
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
sfbench opens the result in your browser, and the command returns right away.
|
|
117
|
+
If sfbench is already running, it opens a new tab.
|
|
118
|
+
|
|
119
|
+
A REST API request or Apex can be filled in the same way:
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
npx -y sfbench -o my-org --rest /services/data/v62.0/sobjects/Account --method POST --body-file account.json
|
|
123
|
+
npx -y sfbench -o my-org --apex-file fix-data.apex
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Writing with AI
|
|
127
|
+
|
|
128
|
+
In the settings (the gear at the top right), choose the AI provider (Anthropic or OpenAI) and enter the model ID and your API key.
|
|
129
|
+
The key is saved in `~/.sfbench/settings.json` on your machine, readable by you only, and never shown in the browser.
|
|
130
|
+
sfbench calls the AI only when you press Generate or Fix with AI, and each call is billed to the key's owner by the provider.
|
|
131
|
+
|
|
132
|
+
What goes to the AI: the names, types and labels of objects and fields, picklist values, what is on the screen (the query or code in the editor, the request in the fields), and the error when fixing.
|
|
133
|
+
The pills under the input show what is sent; untick a pill to keep what is on the screen from being sent.
|
|
134
|
+
SOQL results are never sent.
|
|
135
|
+
A REST API response or an Apex result is sent only when you press "Use this result with AI".
|
|
136
|
+
|
|
137
|
+
## How it works
|
|
138
|
+
|
|
139
|
+
sfbench starts a small web server on your machine (`http://127.0.0.1:7717`, or the next free port up to 7736) and opens its page in your browser.
|
|
140
|
+
The server sends the requests to Salesforce, with an access token it gets from the sf CLI.
|
|
141
|
+
The token never goes to the browser or to any other server.
|
|
142
|
+
The server keeps running until you stop it with `npx sfbench stop`.
|
|
143
|
+
|
|
144
|
+
Settings, recent queries, and the REST API and Apex histories are saved in `~/.sfbench/`.
|
|
145
|
+
The server log is `~/.sfbench/server.log`.
|
|
146
|
+
The log never holds SOQL, request bodies, Apex code or record values.
|
|
147
|
+
|
|
148
|
+
## Development
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
npm install
|
|
152
|
+
npm run dev # starts the web dev server and the local server
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Open `http://localhost:5173/launch/dev`.
|
|
156
|
+
Saved changes show up in the page and the server right away.
|
|
157
|
+
|
|
158
|
+
The design documents in [docs/](docs/) are in Japanese.
|
|
159
|
+
|
|
160
|
+
## License
|
|
161
|
+
|
|
162
|
+
MIT
|
|
163
|
+
|
|
164
|
+
The licenses of the libraries bundled into the web interface are under "Licenses" in the settings (`dist/web/licenses.txt` in the package).
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { agentPrompt, checkAgentOutput } from "./agentTasks.js";
|
|
2
|
+
import { anthropicConversation } from "./anthropic.js";
|
|
3
|
+
import { AiError } from "./errors.js";
|
|
4
|
+
import { openAiConversation } from "./openai.js";
|
|
5
|
+
import { findObject, MAX_READ_BYTES, METADATA_TOOLS, newReadState, readObject, runTool, shown } from "./tools.js";
|
|
6
|
+
/** Local models are out, but large models can still take minutes (docs/ai.md "API"). */
|
|
7
|
+
export const AI_TIMEOUT_MS = 300_000;
|
|
8
|
+
/** The most metadata tool calls in one request (docs/ai-agent.md). */
|
|
9
|
+
export const MAX_TOOL_CALLS = 8;
|
|
10
|
+
/** The most text the tools return in one request, about 30,000 tokens. */
|
|
11
|
+
export const MAX_TOOL_BYTES = 100_000;
|
|
12
|
+
/** Which limit stops the tools: the text they return, or else the number of calls. */
|
|
13
|
+
const limitName = (toolBytes) => (toolBytes >= MAX_TOOL_BYTES ? `the limit of what the tools return (${MAX_TOOL_BYTES / 1000} KB)` : `the limit of ${MAX_TOOL_CALLS} tool calls`);
|
|
14
|
+
const CONVERSATIONS = {
|
|
15
|
+
anthropic: anthropicConversation,
|
|
16
|
+
openai: openAiConversation,
|
|
17
|
+
};
|
|
18
|
+
const encoder = new TextEncoder();
|
|
19
|
+
/** Today in the user's time zone, as YYYY-MM-DD. */
|
|
20
|
+
export function todayIn(timeZone, now = new Date()) {
|
|
21
|
+
return new Intl.DateTimeFormat('en-CA', { timeZone, year: 'numeric', month: '2-digit', day: '2-digit' }).format(now);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The tiny request of "Test connection" (docs/ai-agent.md "接続を確かめる"):
|
|
25
|
+
* proves the model calls tools, submit included, before a screen finds out.
|
|
26
|
+
* The org is not read: the tools find nothing.
|
|
27
|
+
*/
|
|
28
|
+
export const TEST_AGENT_REQUEST = {
|
|
29
|
+
task: 'generateApex',
|
|
30
|
+
input: { request: 'Print the number 1 with System.debug.', language: 'en', timeZone: 'UTC' },
|
|
31
|
+
};
|
|
32
|
+
export const NO_METADATA = { objects: async () => [], describe: async () => undefined };
|
|
33
|
+
/**
|
|
34
|
+
* What the screen named, matched against the org's objects: the open object
|
|
35
|
+
* as a hint, and the fields of the object a failed query read from
|
|
36
|
+
* (docs/ai-agent.md "タスク"). A name the org does not have is left out.
|
|
37
|
+
*/
|
|
38
|
+
async function givenMetadata(req, source, state) {
|
|
39
|
+
if (req.task === 'generateSoql' && req.input.openObject) {
|
|
40
|
+
const open = await findObject(source, req.input.openObject);
|
|
41
|
+
return open ? { openObject: shown(source, open.name) } : {};
|
|
42
|
+
}
|
|
43
|
+
if (req.task === 'fixSoql' && req.input.object) {
|
|
44
|
+
// Within one result, so that the AI can still find fields with what is left.
|
|
45
|
+
const fields = await readObject(source, req.input.object, MAX_READ_BYTES, state);
|
|
46
|
+
return fields ? { fields } : {};
|
|
47
|
+
}
|
|
48
|
+
return {};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Runs one task. `signal` is the caller's (the web UI going away); the
|
|
52
|
+
* timeout is added here. Throws AiError.
|
|
53
|
+
*/
|
|
54
|
+
export async function runAgent(ai, req, source, apiVersion, signal, timeoutMs = AI_TIMEOUT_MS) {
|
|
55
|
+
if (!ai || ai.provider === 'off')
|
|
56
|
+
throw new AiError('AI_NOT_CONFIGURED', 'AI is not set up. Choose an AI in the settings.');
|
|
57
|
+
if (!ai.model.trim())
|
|
58
|
+
throw new AiError('AI_NOT_CONFIGURED', 'No model ID is saved for the AI. Enter it in the settings.');
|
|
59
|
+
// Checked again here, not only when saving: a key never goes to a provider it was not entered for.
|
|
60
|
+
if (!ai.apiKey || ai.keyProvider !== ai.provider)
|
|
61
|
+
throw new AiError('AI_NOT_CONFIGURED', 'No API key is saved for this AI. Enter it in the settings.');
|
|
62
|
+
const timeout = new AbortController();
|
|
63
|
+
const timer = setTimeout(() => timeout.abort(), timeoutMs);
|
|
64
|
+
const both = AbortSignal.any([signal, timeout.signal]);
|
|
65
|
+
let aiCalls = 0;
|
|
66
|
+
let toolCalls = 0;
|
|
67
|
+
let toolBytes = 0;
|
|
68
|
+
// One nudge each: to call submit at all, and to stop reading past the limit.
|
|
69
|
+
let nudgedToSubmit = false;
|
|
70
|
+
let nudgedAtLimit = false;
|
|
71
|
+
try {
|
|
72
|
+
// What the AI has read in this request: objects read in full are not sent again.
|
|
73
|
+
const state = newReadState();
|
|
74
|
+
const given = await givenMetadata(req, source, state);
|
|
75
|
+
// Fields given from the start count against the budget as the tools' results do.
|
|
76
|
+
if (given.fields)
|
|
77
|
+
toolBytes += encoder.encode(given.fields).length;
|
|
78
|
+
const prompt = agentPrompt(req, apiVersion, todayIn(req.input.timeZone), given);
|
|
79
|
+
const submit = { name: 'submit', description: 'Gives the final answer. Call it once, when done.', schema: prompt.submitSchema };
|
|
80
|
+
const conversation = CONVERSATIONS[ai.provider]({
|
|
81
|
+
apiKey: ai.apiKey,
|
|
82
|
+
model: ai.model,
|
|
83
|
+
system: prompt.system,
|
|
84
|
+
user: prompt.user,
|
|
85
|
+
tools: [...METADATA_TOOLS, submit],
|
|
86
|
+
signal: both,
|
|
87
|
+
});
|
|
88
|
+
// Each turn either calls tools (counted, so it ends) or uses up a nudge.
|
|
89
|
+
for (;;) {
|
|
90
|
+
const turn = await conversation.next();
|
|
91
|
+
aiCalls++;
|
|
92
|
+
const answer = turn.calls.find((c) => c.name === 'submit');
|
|
93
|
+
if (answer)
|
|
94
|
+
return { output: checkAgentOutput(req.task, answer.input), aiCalls, toolCalls };
|
|
95
|
+
if (turn.calls.length === 0) {
|
|
96
|
+
if (nudgedToSubmit)
|
|
97
|
+
throw new AiError('AI_BAD_OUTPUT', 'The AI answered without calling submit.', turn.text);
|
|
98
|
+
nudgedToSubmit = true;
|
|
99
|
+
conversation.say('Give your answer by calling the submit tool.');
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (toolCalls >= MAX_TOOL_CALLS || toolBytes >= MAX_TOOL_BYTES) {
|
|
103
|
+
if (nudgedAtLimit)
|
|
104
|
+
throw new AiError('AI_BAD_OUTPUT', `The AI kept reading the org's metadata past the limit (${limitName(toolBytes)}).`);
|
|
105
|
+
nudgedAtLimit = true;
|
|
106
|
+
// Every call of the turn needs its answer before anything else can be said.
|
|
107
|
+
conversation.answer(turn.calls.map((c) => ({ id: c.id, content: `Not run: ${limitName(toolBytes)} is reached. Call submit now with your best answer.` })));
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const results = [];
|
|
111
|
+
for (const call of turn.calls) {
|
|
112
|
+
// The screen went away (Cancel, a closed tab): no more reads of the org.
|
|
113
|
+
if (both.aborted)
|
|
114
|
+
throw both.reason;
|
|
115
|
+
toolCalls++;
|
|
116
|
+
const over = toolCalls > MAX_TOOL_CALLS || toolBytes >= MAX_TOOL_BYTES;
|
|
117
|
+
const content = over ? `Not run: ${limitName(toolBytes)} is reached. Call submit now.` : await runTool(source, call.name, call.input, MAX_TOOL_BYTES - toolBytes, state);
|
|
118
|
+
toolBytes += encoder.encode(content).length;
|
|
119
|
+
results.push({ id: call.id, content });
|
|
120
|
+
}
|
|
121
|
+
conversation.answer(results);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
if (timeout.signal.aborted)
|
|
126
|
+
throw new AiError('AI_TIMEOUT', `The AI did not answer within ${Math.round(timeoutMs / 1000)} seconds.`);
|
|
127
|
+
throw e;
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
clearTimeout(timer);
|
|
131
|
+
}
|
|
132
|
+
}
|