@senpiper/sp-agent 0.1.9 → 0.1.11
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/INSTALL.md +37 -26
- package/README.md +13 -13
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +4 -4
- package/dist/cli.js.map +1 -1
- package/dist/distribution.d.ts +2 -2
- package/dist/distribution.d.ts.map +1 -1
- package/dist/distribution.js +2 -3
- package/dist/distribution.js.map +1 -1
- package/dist/extension-identity.generated.d.ts +3 -3
- package/dist/extension-identity.generated.d.ts.map +1 -1
- package/dist/extension-identity.generated.js +3 -3
- package/dist/extension-identity.generated.js.map +1 -1
- package/dist/install-sp.sh +9 -9
- package/dist/release-manager.d.ts.map +1 -1
- package/dist/release-manager.js +2 -2
- package/dist/release-manager.js.map +1 -1
- package/dist/runtime-cli.d.ts.map +1 -1
- package/dist/runtime-cli.js +4 -4
- package/dist/runtime-cli.js.map +1 -1
- package/dist/sdk.d.ts +1 -1
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +5 -5
- package/dist/sdk.js.map +1 -1
- package/dist/session-manager.d.ts +1 -1
- package/dist/session-manager.d.ts.map +1 -1
- package/dist/session-manager.js +15 -15
- package/dist/session-manager.js.map +1 -1
- package/dist/sp-web.d.ts.map +1 -1
- package/dist/sp-web.js +2 -2
- package/dist/sp-web.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/node_modules/@senpiper/pi-coding-agent/dist/bundle/chunks/{chunk-IK73Y232.js → chunk-VVKE4QZJ.js} +1 -1
- package/node_modules/@senpiper/pi-coding-agent/dist/bundle/cli.js +1 -1
- package/node_modules/@senpiper/pi-coding-agent/dist/bundle/index.js +1 -1
- package/node_modules/@senpiper/pi-coding-agent/dist/bundle/rpc-entry.js +1 -1
- package/node_modules/@senpiper/pi-coding-agent/node_modules/@earendil-works/pi-ai/dist/providers/data/.manifest.json +1 -1
- package/node_modules/@senpiper/pi-coding-agent/node_modules/@earendil-works/pi-ai/dist/providers/data/amazon-bedrock.json +1 -1
- package/node_modules/@senpiper/pi-coding-agent/node_modules/@earendil-works/pi-ai/dist/providers/data/baseten.json +1 -1
- package/node_modules/@senpiper/pi-coding-agent/node_modules/@earendil-works/pi-ai/dist/providers/data/fireworks.json +1 -1
- package/node_modules/@senpiper/pi-coding-agent/node_modules/@earendil-works/pi-ai/dist/providers/data/huggingface.json +1 -1
- package/node_modules/@senpiper/pi-coding-agent/node_modules/@earendil-works/pi-ai/dist/providers/data/kimi-coding.json +1 -1
- package/node_modules/@senpiper/pi-coding-agent/node_modules/@earendil-works/pi-ai/dist/providers/data/nvidia.json +1 -1
- package/node_modules/@senpiper/pi-coding-agent/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode-go.json +1 -1
- package/node_modules/@senpiper/pi-coding-agent/node_modules/@earendil-works/pi-ai/dist/providers/data/openrouter.json +1 -1
- package/node_modules/@senpiper/pi-coding-agent/node_modules/@earendil-works/pi-ai/dist/providers/data/qwen-token-plan-cn.json +1 -1
- package/node_modules/@senpiper/pi-coding-agent/node_modules/@earendil-works/pi-ai/dist/providers/data/qwen-token-plan.json +1 -1
- package/node_modules/@senpiper/pi-coding-agent/node_modules/@earendil-works/pi-ai/dist/providers/data/together.json +1 -1
- package/node_modules/@senpiper/pi-coding-agent/node_modules/@earendil-works/pi-ai/dist/providers/data/vercel-ai-gateway.json +1 -1
- package/node_modules/@senpiper/pi-coding-agent/package.json +1 -1
- package/node_modules/@senpiper/sp-agent-extensions/CHANGELOG.md +17 -0
- package/node_modules/@senpiper/sp-agent-extensions/CONCURRENT_SESSIONS.md +9 -9
- package/node_modules/@senpiper/sp-agent-extensions/TERMINAL_WEB_BRIDGE.md +29 -10
- package/node_modules/@senpiper/sp-agent-extensions/dist/cli-commands.d.ts +0 -6
- package/node_modules/@senpiper/sp-agent-extensions/dist/cli-commands.d.ts.map +1 -1
- package/node_modules/@senpiper/sp-agent-extensions/dist/cli-commands.js +7 -86
- package/node_modules/@senpiper/sp-agent-extensions/dist/cli-commands.js.map +1 -1
- package/node_modules/@senpiper/sp-agent-extensions/dist/commands.d.ts +2 -2
- package/node_modules/@senpiper/sp-agent-extensions/dist/commands.d.ts.map +1 -1
- package/node_modules/@senpiper/sp-agent-extensions/dist/commands.js +2 -2
- package/node_modules/@senpiper/sp-agent-extensions/dist/commands.js.map +1 -1
- package/node_modules/@senpiper/sp-agent-extensions/dist/gateway.d.ts +1 -1
- package/node_modules/@senpiper/sp-agent-extensions/dist/gateway.d.ts.map +1 -1
- package/node_modules/@senpiper/sp-agent-extensions/dist/gateway.js +28 -17
- package/node_modules/@senpiper/sp-agent-extensions/dist/gateway.js.map +1 -1
- package/node_modules/@senpiper/sp-agent-extensions/dist/index.d.ts +1 -3
- package/node_modules/@senpiper/sp-agent-extensions/dist/index.d.ts.map +1 -1
- package/node_modules/@senpiper/sp-agent-extensions/dist/index.js +1 -3
- package/node_modules/@senpiper/sp-agent-extensions/dist/index.js.map +1 -1
- package/node_modules/@senpiper/sp-agent-extensions/dist/interactions.d.ts +0 -2
- package/node_modules/@senpiper/sp-agent-extensions/dist/interactions.d.ts.map +1 -1
- package/node_modules/@senpiper/sp-agent-extensions/dist/interactions.js +0 -3
- package/node_modules/@senpiper/sp-agent-extensions/dist/interactions.js.map +1 -1
- package/node_modules/@senpiper/sp-agent-extensions/dist/login-command.d.ts.map +1 -1
- package/node_modules/@senpiper/sp-agent-extensions/dist/login-command.js +10 -20
- package/node_modules/@senpiper/sp-agent-extensions/dist/login-command.js.map +1 -1
- package/node_modules/@senpiper/sp-agent-extensions/dist/sp-web-launcher.d.ts +27 -0
- package/node_modules/@senpiper/sp-agent-extensions/dist/sp-web-launcher.d.ts.map +1 -1
- package/node_modules/@senpiper/sp-agent-extensions/dist/sp-web-launcher.js +270 -28
- package/node_modules/@senpiper/sp-agent-extensions/dist/sp-web-launcher.js.map +1 -1
- package/node_modules/@senpiper/sp-agent-extensions/dist/startup-header.d.ts.map +1 -1
- package/node_modules/@senpiper/sp-agent-extensions/dist/startup-header.js +13 -20
- package/node_modules/@senpiper/sp-agent-extensions/dist/startup-header.js.map +1 -1
- package/node_modules/@senpiper/sp-agent-extensions/dist/terminal-web-bridge.d.ts +1 -1
- package/node_modules/@senpiper/sp-agent-extensions/dist/terminal-web-bridge.d.ts.map +1 -1
- package/node_modules/@senpiper/sp-agent-extensions/dist/terminal-web-bridge.js +11 -9
- package/node_modules/@senpiper/sp-agent-extensions/dist/terminal-web-bridge.js.map +1 -1
- package/node_modules/@senpiper/sp-agent-extensions/dist/web-command.d.ts.map +1 -1
- package/node_modules/@senpiper/sp-agent-extensions/dist/web-command.js +36 -10
- package/node_modules/@senpiper/sp-agent-extensions/dist/web-command.js.map +1 -1
- package/node_modules/@senpiper/sp-agent-extensions/package.json +3 -5
- package/package.json +7 -6
- package/node_modules/@senpiper/sp-agent-extensions/dist/onboarding-command.d.ts +0 -4
- package/node_modules/@senpiper/sp-agent-extensions/dist/onboarding-command.d.ts.map +0 -1
- package/node_modules/@senpiper/sp-agent-extensions/dist/onboarding-command.js +0 -67
- package/node_modules/@senpiper/sp-agent-extensions/dist/onboarding-command.js.map +0 -1
- package/node_modules/@senpiper/sp-agent-extensions/dist/onboarding-installation.d.ts +0 -14
- package/node_modules/@senpiper/sp-agent-extensions/dist/onboarding-installation.d.ts.map +0 -1
- package/node_modules/@senpiper/sp-agent-extensions/dist/onboarding-installation.js +0 -32
- package/node_modules/@senpiper/sp-agent-extensions/dist/onboarding-installation.js.map +0 -1
- package/node_modules/@senpiper/sp-agent-extensions/dist/onboarding.d.ts +0 -39
- package/node_modules/@senpiper/sp-agent-extensions/dist/onboarding.d.ts.map +0 -1
- package/node_modules/@senpiper/sp-agent-extensions/dist/onboarding.js +0 -311
- package/node_modules/@senpiper/sp-agent-extensions/dist/onboarding.js.map +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@senpiper/pi-coding-agent",
|
|
3
3
|
"version": "0.84.4",
|
|
4
|
-
"description": "Senpiper build of the generic Pi coding-agent runtime seams required by
|
|
4
|
+
"description": "Senpiper build of the generic Pi coding-agent runtime seams required by Senpiper Workbench",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"piConfig": {
|
|
7
7
|
"name": "sp",
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
## [0.1.5] - 2026-09-16
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added `/web on` and `/web off` to the terminal agent. `/web off` stops the managed web sidecar for the current profile and waits up to eight seconds for the process to exit. Terminal sessions, agent sessions, and Senpiper Gateway keep running, and the next `/web` or interactive launch starts the sidecar again.
|
|
10
|
+
- Added a per-launch identity token to new sidecar records. Stopping verifies the live process against the token, falls back to an exact command line and start time for older records, and fails closed when the identity cannot be read or does not match.
|
|
11
|
+
- Added an ASCII WORKBENCH banner to the interactive startup header on wide terminals. Narrow terminals show a compact WORKBENCH wordmark. Both are followed by a `by senpiper` attribution.
|
|
12
|
+
- Added argument completions for `/web on` and `/web off`, and a warning at session start when another terminal is already starting or stopping the sidecar.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- Renamed the product to Senpiper Workbench in headers, prompts, notifications, and gateway messages. The model gateway now displays as Senpiper Gateway.
|
|
17
|
+
- `/web` rejects unknown arguments with usage text and reports clearer disabled, failed, and timed-out stop errors.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# Concurrent
|
|
1
|
+
# Concurrent Senpiper Workbench sessions
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Senpiper Workbench supports independent concurrent sessions within one instance. Browser selection does not replace the terminal session or stop another web session.
|
|
4
4
|
|
|
5
5
|
## Wire contract
|
|
6
6
|
|
|
@@ -14,7 +14,7 @@ Send these commands to any authenticated descriptor belonging to the instance:
|
|
|
14
14
|
|
|
15
15
|
The result is returned only after the child descriptor is published in the existing discovery directory. Find it by `sessionId`, authenticate with its token, and use the existing state, transcript, subscription, prompt, steer, follow-up, abort, model, thinking, title, and question commands. Each descriptor retains its own generation and replay sequence. No envelope or descriptor format changed.
|
|
16
16
|
|
|
17
|
-
`state.capabilities.newSession` and `resumeSession` are true when the
|
|
17
|
+
`state.capabilities.newSession` and `resumeSession` are true when the Senpiper Workbench distribution supplies its instance manager. An extension loaded without that manager keeps these capabilities false.
|
|
18
18
|
|
|
19
19
|
Repeated resumes of the same file return the existing session ID, including concurrent requests and symlink aliases. Resuming the terminal's current file returns the terminal session ID without opening another writer. Requests racing a terminal acquisition fail and may be retried after it settles.
|
|
20
20
|
|
|
@@ -22,7 +22,7 @@ Browser switching is client-local. Disconnecting from B and subscribing to C lea
|
|
|
22
22
|
|
|
23
23
|
## Ownership and lifetime
|
|
24
24
|
|
|
25
|
-
`packages/sp-agent/src/sdk.ts` supplies the verified
|
|
25
|
+
`packages/sp-agent/src/sdk.ts` supplies the verified Senpiper Workbench factory. It uses the same required-extension identity, provider policy, and resource ownership checks as `createSpAgentSession`; the bridge does not recreate provider policy.
|
|
26
26
|
|
|
27
27
|
`SpSessionManager` reserves canonical file paths synchronously before opening files or awaiting construction. Failed construction releases the reservation. Hard-linked files are rejected to avoid alternate names bypassing ownership.
|
|
28
28
|
|
|
@@ -34,25 +34,25 @@ Explicit manager disposal and terminal quit stop bridges, cancel questions, abor
|
|
|
34
34
|
|
|
35
35
|
## Permanent deletion and writer leases
|
|
36
36
|
|
|
37
|
-
`capabilities.deleteSession` is true only when the native
|
|
37
|
+
`capabilities.deleteSession` is true only when the native Senpiper Workbench distribution deletion provider is active. The extension alone advertises false and rejects the command. Deletion never starts an SDK writer for inactive history.
|
|
38
38
|
|
|
39
39
|
For a managed child, deletion checks prompt preflight, active runs, queued messages, questions, compaction, retries, bash, and pending bridge commands. A busy child returns `busy`; deletion does not abort it to make it eligible. The owner gates new mutations, retains its existing file reservation, disposes only that child, and unlinks only that JSONL. Parent, descendant, sibling, and terminal sessions remain untouched. Deleting the current terminal session returns `terminal_root`. Run `/new` in that terminal first; deletion never shuts down the whole instance.
|
|
40
40
|
|
|
41
41
|
Inactive history must be inside the canonical configured history root. CLI configuration includes `--session-dir`, the session-directory environment override, and the configured settings fallback. SDK managers default to `$SP_CODING_AGENT_DIR/sessions` or `SP_CODING_AGENT_SESSION_DIR`; hosts using a different history root must call `setHistoryRoot()` explicitly. An arbitrary selected session's parent directory does not become an authorized history root. Explicit paths must be canonical absolute `.jsonl` paths to regular files with one hard link. Symlink aliases, escapes, mismatched header IDs, and ambiguous ID-only lookups are rejected. Header validation is bounded to 1 MiB and does not repair files.
|
|
42
42
|
|
|
43
|
-
The generic `MainOptions.sessionWriterLeaseProvider` hook runs before CLI session opening. The
|
|
43
|
+
The generic `MainOptions.sessionWriterLeaseProvider` hook runs before CLI session opening. The Senpiper Workbench SDK installs the same provider on import, before callers construct persistent `SessionManager`s. Passing a persistent manager constructed before Senpiper Workbench participation is rejected. New files, resume, import destinations, branch creation, and JSONL export use the provider. Each persistent manager retains its lease until disposal, not merely until resume finishes. Failed construction releases its lease. Disposed or transaction-reserved writers reject further writes.
|
|
44
44
|
|
|
45
45
|
All participating processes use exclusive, non-expiring files under `.sp-writer-leases` beside the JSONL directory. Locks are independent of the JSONL inode and canonicalize aliases. A managed deletion retains the exact existing lease through writer disposal and unlink, with no unlock/relock gap. A deletion tombstone prevents a previously selected resume path from recreating the deleted file. Normal disposal and process exit release leases. A paused process never loses its lease to a heartbeat timeout.
|
|
46
46
|
|
|
47
|
-
Crash-orphaned locks fail closed. There is no automatic stale-lock takeover. If
|
|
47
|
+
Crash-orphaned locks fail closed. There is no automatic stale-lock takeover. If a Senpiper Workbench process is killed without exit cleanup, stop all Senpiper Workbench processes using that history before an operator removes its orphan `.lock` files. Do not remove `.deleted` tombstones or live locks. This availability tradeoff prevents paused writers from resuming after deletion.
|
|
48
48
|
|
|
49
49
|
Self-deletion preserves bridge connections until its success or error response drains, while rejecting new commands. Other deletions close target sockets during shutdown. Client disconnection does not cancel deletion or release the transaction reservation early.
|
|
50
50
|
|
|
51
51
|
### Required cutover
|
|
52
52
|
|
|
53
|
-
After installing this version, **restart every
|
|
53
|
+
After installing this version, **restart every Senpiper Workbench instance**, including terminal, print/RPC, managed-session, and SDK hosts. Updating files on disk does not upgrade a running writer. The owner rejects discoverable live legacy Senpiper Workbench descriptors without `writerLeaseVersion: 1`, even when they currently select another session. PID checks are limited to these discoverable owners; deletion does not attempt to prove the absence of every OS writer.
|
|
54
54
|
|
|
55
|
-
The guarantee covers cooperating updated
|
|
55
|
+
The guarantee covers cooperating updated Senpiper Workbench writers. Arbitrary external programs, nonparticipating SDK hosts, old processes that were not restarted and are not discoverable, and direct filesystem writes are outside it. Do not use native deletion during a mixed-version cutover.
|
|
56
56
|
|
|
57
57
|
Read-only web history browsing must not acquire writer leases. Use `SessionManager.open(path, undefined, undefined, { readOnly: true })` or `openReadonly(path)`. These paths do not repair or migrate files on disk and cannot persist mutations. Live browser commands still go through the native owner. Fork and arbitrary extension UI dialogs remain outside the web protocol.
|
|
58
58
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Senpiper Workbench terminal web bridge protocol
|
|
2
2
|
|
|
3
|
-
Interactive `sp` sessions expose their live state through same-user Unix sockets. The
|
|
3
|
+
Interactive `sp` sessions expose their live state through same-user Unix sockets. The Senpiper Workbench process owns the terminal session and any independent web sessions it creates. Each has its own descriptor, `AgentSession`, and JSONL writer. A web client must send live commands through the corresponding socket, not open another writer.
|
|
4
4
|
|
|
5
5
|
## Discovery
|
|
6
6
|
|
|
@@ -40,7 +40,7 @@ A response repeats `id` and has either `ok: true` with `result`, or `ok: false`
|
|
|
40
40
|
| `state` | none | Returns session, run, model, thinking, sequence, and capability state. |
|
|
41
41
|
| `transcript` | `cursor`, `limit` | Returns up to 100 read-only session entries. |
|
|
42
42
|
| `models` | none | Returns the models available within the terminal session's model scope. |
|
|
43
|
-
| `generate_title` | none | Generates an English title with the active
|
|
43
|
+
| `generate_title` | none | Generates an English title with the active Senpiper Gateway model and applies it through the terminal owner. |
|
|
44
44
|
| `subscribe` | `afterSequence` | Replays retained events after the sequence and subscribes to new events. Returns `event_gap` if the requested point is no longer retained. |
|
|
45
45
|
| `prompt` | `text`, optional `images`, optional `expandPromptTemplates` | Starts a turn while idle. Text may be omitted with images. |
|
|
46
46
|
| `steer` | `text`, optional `images` | Steers the active turn. Fails while idle. Text may be omitted with images. |
|
|
@@ -53,11 +53,11 @@ A response repeats `id` and has either `ok: true` with `result`, or `ok: false`
|
|
|
53
53
|
| `resume_session` | `sessionFile`, an absolute path | Acquires an independent web session or returns its existing `{ sessionId }`. Returns the terminal ID for its current file without reopening it. |
|
|
54
54
|
| `delete_session` | `sessionId`, optional canonical absolute `sessionFile`, `confirmed: true` | Permanently deletes only the selected idle managed child or inactive history file. Returns `{ sessionId, deleted: true }`. |
|
|
55
55
|
|
|
56
|
-
Creation and resume require the instance-owned factory supplied by
|
|
56
|
+
Creation and resume require the instance-owned factory supplied by Senpiper Workbench. `state.capabilities.newSession` and `resumeSession` advertise availability. Browser selection and disconnection do not stop runs. See [concurrent sessions](CONCURRENT_SESSIONS.md) for acquisition and lifetime rules. Fork, direct JSONL mutation, credential changes, and arbitrary model construction remain unsupported.
|
|
57
57
|
|
|
58
58
|
`state.capabilities.deleteSession` is true only with the native deletion provider. Deletion rejects busy, queued, or question-owning targets. The current terminal root returns `terminal_root`; the user must run `/new` there first. Explicit confirmation is mandatory. `writer_owned`, `legacy_owner`, `invalid_path`, `invalid_session`, and `session_deleted` describe ownership, cutover, validation, and tombstone failures. Filesystem failures can also be returned. No descendants or sibling sessions are deleted. Self-delete responses drain before their socket closes, and disconnecting does not cancel the transaction.
|
|
59
59
|
|
|
60
|
-
Restart **all
|
|
60
|
+
Restart **all Senpiper Workbench instances after installation** before using deletion. Updated writers share lifetime leases; discoverable live legacy owners are rejected. External/nonparticipating SDK and direct filesystem writers are outside the guarantee. See the required cutover and orphan-lock procedure in [concurrent sessions](CONCURRENT_SESSIONS.md).
|
|
61
61
|
|
|
62
62
|
Events use `type: "event"` and include `generation`, monotonic `sequence`, `category`, `event`, and `payload`. Categories cover session, agent, message, tool, model, and thinking lifecycle events. The replay buffer and client count are bounded. The bridge disconnects clients that exceed frame or output-buffer limits.
|
|
63
63
|
|
|
@@ -162,11 +162,21 @@ Successful responses return `{ "accepted": true }`. The next `question_pending`
|
|
|
162
162
|
|
|
163
163
|
Questions stay pending across web disconnects. Read a snapshot on reconnect and subscribe after its sequence to close the snapshot/subscription race. Process events in sequence order; only clear a displayed question when a resolved event matches its interaction ID. Abort, session shutdown, reload, and generation replacement cancel the questionnaire rather than restoring it. No question is reconstructed from JSONL, and no second session is created.
|
|
164
164
|
|
|
165
|
-
This protocol covers `ask_user_question`, not arbitrary extension UI prompts such as login
|
|
165
|
+
This protocol covers `ask_user_question`, not arbitrary extension UI prompts such as login. Oversized question events follow the existing frame limit; terminal dialogs remain available, but the web snapshot can return `frame_too_large`. The bridge does not split questions into multiple frames.
|
|
166
166
|
|
|
167
|
-
##
|
|
167
|
+
## Senpiper Workbench web sidecar
|
|
168
168
|
|
|
169
|
-
The extension starts one detached sidecar per configured
|
|
169
|
+
The extension starts one detached sidecar per configured agent directory and reuses its private PID record while that process is alive.
|
|
170
|
+
|
|
171
|
+
`/web` and `/web on` ensure that sidecar is running and open it in the browser.
|
|
172
|
+
`/web off` stops only the managed sidecar for the current profile, without
|
|
173
|
+
stopping the terminal bridges, agent sessions, or Gateway. This is not a
|
|
174
|
+
persistent disable: another interactive agent launch can start the sidecar
|
|
175
|
+
again. All browsers connected to that profile's shared server disconnect;
|
|
176
|
+
workspace shell processes owned by the web server may end. Manually launched
|
|
177
|
+
servers must be stopped in their own terminal.
|
|
178
|
+
|
|
179
|
+
Configuration:
|
|
170
180
|
|
|
171
181
|
| Variable | Meaning |
|
|
172
182
|
|---|---|
|
|
@@ -174,10 +184,19 @@ The extension starts one detached sidecar per configured SP agent directory and
|
|
|
174
184
|
| `SP_WEB_ARGS_JSON` | JSON string array of arguments. The launcher does not use a shell. |
|
|
175
185
|
| `SP_WEB_DISABLE` | `1`, `true`, or `yes` disables launch. |
|
|
176
186
|
|
|
177
|
-
Each `SP_WEB_*` setting takes precedence over its old `SP_PI_WEB_*` alias, including an explicitly empty value. Without an executable override, the launcher resolves `@senpiper/sp-web/bin/sp-web.js` from the extension module, then from the
|
|
187
|
+
Each `SP_WEB_*` setting takes precedence over its old `SP_PI_WEB_*` alias, including an explicitly empty value. Without an executable override, the launcher resolves `@senpiper/sp-web/bin/sp-web.js` from the extension module, then from the Senpiper Workbench executable location. The current source pins `@senpiper/sp-web@0.1.1` as a normal registry dependency, installed automatically by the same global Senpiper Workbench install command, not bundled in the Senpiper Workbench tarball. The resolved absolute entry runs with the current Node executable, or external `node` from PATH when Senpiper Workbench runs under Bun. Compiled Bun's executable anchor locates the npm dependency tree even when its module URL is virtual. Resolution does not use the project working directory or a global `pi-web` command.
|
|
178
188
|
|
|
179
189
|
Standalone archives without the npm package fall back to `sp-web` on PATH. Missing executables produce a warning with `SP_WEB_EXECUTABLE` setup guidance. Bun launches of the installed entry require external Node on PATH. A successful launch result records a spawned PID, not HTTP readiness.
|
|
180
190
|
|
|
181
|
-
The launcher always adds `--no-open`, sets `SP_WEB_NO_OPEN=1` and `SP_TERMINAL_BRIDGE_ONLY=1`, and passes `SP_TERMINAL_BRIDGE_DISCOVERY_DIR` and `SP_TERMINAL_BRIDGE_PROTOCOL` to the child.
|
|
191
|
+
The launcher always adds `--no-open`, sets `SP_WEB_NO_OPEN=1` and `SP_TERMINAL_BRIDGE_ONLY=1`, and passes `SP_TERMINAL_BRIDGE_DISCOVERY_DIR` and `SP_TERMINAL_BRIDGE_PROTOCOL` to the child. The Senpiper Workbench web workspace must use the authenticated descriptors for live commands, never construct another writer for the same session.
|
|
182
192
|
|
|
183
193
|
The canonical process record is `sp-web-sidecar.json`. Live records under either that name or `pi-web-sidecar.json` are reused without rewriting them or restarting their processes. The cooperative lock remains `pi-web-sidecar.lock`; new launches also mirror their private PID record to `pi-web-sidecar.json` so older launchers reuse them. Both process records are `0600` and neither represents a session writer. This launcher migration does not change writer leases, IPC authentication, question ownership, or historical JSONL.
|
|
194
|
+
|
|
195
|
+
New records include a random per-launch identity token, also passed to the
|
|
196
|
+
sidecar in `SP_WEB_SIDECAR_TOKEN`. Stop verifies the live process against that
|
|
197
|
+
token before sending `SIGTERM`; older records require an exact command-line
|
|
198
|
+
and process-start-time match. Unreadable or mismatched identity fails closed.
|
|
199
|
+
Start and stop share the cooperative lock. Stop waits up to eight seconds for
|
|
200
|
+
the wrapper to exit before removing both records; timeouts retain the records
|
|
201
|
+
and report failure. Custom executable wrappers must forward `SIGTERM` and wait
|
|
202
|
+
for their server children, as the bundled `sp-web` wrapper does.
|
|
@@ -4,12 +4,6 @@ interface LoginOptions {
|
|
|
4
4
|
help?: boolean;
|
|
5
5
|
}
|
|
6
6
|
export declare function parseLoginOptions(args: string[]): LoginOptions;
|
|
7
|
-
export declare function runCliOnboarding(): Promise<{
|
|
8
|
-
completed: boolean;
|
|
9
|
-
installedSkillIds: string[];
|
|
10
|
-
failedSkillIds: string[];
|
|
11
|
-
results: import("./onboarding.ts").SkillInstallResult[];
|
|
12
|
-
}>;
|
|
13
7
|
export declare function handleSpCommand(args: string[]): Promise<boolean>;
|
|
14
8
|
export {};
|
|
15
9
|
//# sourceMappingURL=cli-commands.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli-commands.d.ts","sourceRoot":"","sources":["../src/cli-commands.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"cli-commands.d.ts","sourceRoot":"","sources":["../src/cli-commands.ts"],"names":[],"mappings":"AAUA,UAAU,YAAY;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;CACf;AAWD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,gBAqB/C;AAkBD,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,oBAkCnD","sourcesContent":["import { stdin as input, stdout as output } from \"node:process\";\nimport { createInterface } from \"node:readline/promises\";\nimport {\n\tfetchSpGatewayModels,\n\tnormalizeSpGatewayBaseUrl,\n\tremoveSpGatewayLogin,\n\twriteSpGatewayLogin,\n\twriteSpGatewayState,\n} from \"./gateway.ts\";\n\ninterface LoginOptions {\n\tgatewayUrl?: string;\n\tapiKey?: string;\n\thelp?: boolean;\n}\n\nfunction printHelp() {\n\tconsole.log(`Senpiper Workbench gateway setup\n\nUsage:\n sp login [--gateway-url <url>] [--api-key <key>]\n sp logout\n`);\n}\n\nexport function parseLoginOptions(args: string[]) {\n\tconst options: LoginOptions = {};\n\tfor (let index = 0; index < args.length; index++) {\n\t\tconst argument = args[index];\n\t\tif (argument === \"-h\" || argument === \"--help\") {\n\t\t\toptions.help = true;\n\t\t\tcontinue;\n\t\t}\n\t\tif (argument === \"--gateway-url\") {\n\t\t\toptions.gatewayUrl = args[++index];\n\t\t\tif (!options.gatewayUrl) throw new Error(\"--gateway-url requires a value\");\n\t\t\tcontinue;\n\t\t}\n\t\tif (argument === \"--api-key\") {\n\t\t\toptions.apiKey = args[++index];\n\t\t\tif (!options.apiKey) throw new Error(\"--api-key requires a value\");\n\t\t\tcontinue;\n\t\t}\n\t\tthrow new Error(`Unknown login option: ${argument}`);\n\t}\n\treturn options;\n}\n\nasync function promptForLogin(options: LoginOptions) {\n\tif (options.gatewayUrl && options.apiKey) return { gatewayUrl: options.gatewayUrl, apiKey: options.apiKey };\n\tif (!input.isTTY || !output.isTTY) {\n\t\tthrow new Error(\"Missing gateway credentials. Pass --gateway-url and --api-key in non-interactive mode.\");\n\t}\n\tconst prompt = createInterface({ input, output });\n\ttry {\n\t\treturn {\n\t\t\tgatewayUrl: options.gatewayUrl ?? (await prompt.question(\"Senpiper Gateway URL: \")),\n\t\t\tapiKey: options.apiKey ?? (await prompt.question(\"Senpiper Gateway API key: \")),\n\t\t};\n\t} finally {\n\t\tprompt.close();\n\t}\n}\n\nexport async function handleSpCommand(args: string[]) {\n\tconst [command, ...rest] = args;\n\tif (command !== \"login\" && command !== \"logout\") return false;\n\ttry {\n\t\tif (command === \"logout\") {\n\t\t\tif (rest.some((argument) => argument === \"-h\" || argument === \"--help\")) {\n\t\t\t\tprintHelp();\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tif (rest.length > 0) throw new Error(`Unknown logout option: ${rest[0]}`);\n\t\t\tremoveSpGatewayLogin();\n\t\t\tconsole.log(\"Removed stored Senpiper Gateway API key.\");\n\t\t\treturn true;\n\t\t}\n\n\t\tconst options = parseLoginOptions(rest);\n\t\tif (options.help) {\n\t\t\tprintHelp();\n\t\t\treturn true;\n\t\t}\n\t\tconst { gatewayUrl, apiKey } = await promptForLogin(options);\n\t\tconst baseUrl = normalizeSpGatewayBaseUrl(gatewayUrl);\n\t\tconst models = await fetchSpGatewayModels(baseUrl, apiKey);\n\t\twriteSpGatewayState(baseUrl, models);\n\t\twriteSpGatewayLogin(apiKey, models[0]!.id);\n\t\tconsole.log(`Connected to Senpiper Gateway at ${baseUrl}`);\n\t\tconsole.log(`Synced ${models.length} model${models.length === 1 ? \"\" : \"s\"}.`);\n\t\tconsole.log(`Selected model: ${models[0]!.id}`);\n\t\treturn true;\n\t} catch (error) {\n\t\tconsole.error(`Error: ${error instanceof Error ? error.message : String(error)}`);\n\t\tprocess.exitCode = 1;\n\t\treturn true;\n\t}\n}\n"]}
|
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
import { stdin as input, stdout as output } from "node:process";
|
|
2
2
|
import { createInterface } from "node:readline/promises";
|
|
3
|
-
import { fetchSpGatewayModels,
|
|
4
|
-
import { fetchSkillCatalog, hasCompletedOnboarding, selectCatalogSkills } from "./onboarding.js";
|
|
5
|
-
import { installOnboardingSelection } from "./onboarding-installation.js";
|
|
3
|
+
import { fetchSpGatewayModels, normalizeSpGatewayBaseUrl, removeSpGatewayLogin, writeSpGatewayLogin, writeSpGatewayState, } from "./gateway.js";
|
|
6
4
|
function printHelp() {
|
|
7
|
-
console.log(`
|
|
5
|
+
console.log(`Senpiper Workbench gateway setup
|
|
8
6
|
|
|
9
7
|
Usage:
|
|
10
8
|
sp login [--gateway-url <url>] [--api-key <key>]
|
|
11
9
|
sp logout
|
|
12
|
-
sp onboarding
|
|
13
10
|
`);
|
|
14
11
|
}
|
|
15
12
|
export function parseLoginOptions(args) {
|
|
@@ -45,83 +42,19 @@ async function promptForLogin(options) {
|
|
|
45
42
|
const prompt = createInterface({ input, output });
|
|
46
43
|
try {
|
|
47
44
|
return {
|
|
48
|
-
gatewayUrl: options.gatewayUrl ?? (await prompt.question("
|
|
49
|
-
apiKey: options.apiKey ?? (await prompt.question("
|
|
45
|
+
gatewayUrl: options.gatewayUrl ?? (await prompt.question("Senpiper Gateway URL: ")),
|
|
46
|
+
apiKey: options.apiKey ?? (await prompt.question("Senpiper Gateway API key: ")),
|
|
50
47
|
};
|
|
51
48
|
}
|
|
52
49
|
finally {
|
|
53
50
|
prompt.close();
|
|
54
51
|
}
|
|
55
52
|
}
|
|
56
|
-
async function promptYesNo(prompt, question) {
|
|
57
|
-
while (true) {
|
|
58
|
-
const answer = (await prompt.question(`${question} [Y/n] `)).trim().toLowerCase();
|
|
59
|
-
if (!answer || answer === "y" || answer === "yes")
|
|
60
|
-
return true;
|
|
61
|
-
if (answer === "n" || answer === "no")
|
|
62
|
-
return false;
|
|
63
|
-
console.log("Enter y or n.");
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
async function promptRoles(prompt, catalog) {
|
|
67
|
-
console.log("\nDeveloper roles");
|
|
68
|
-
for (const [index, role] of catalog.roles.entries())
|
|
69
|
-
console.log(` ${index + 1}. ${role.title}`);
|
|
70
|
-
while (true) {
|
|
71
|
-
const answer = (await prompt.question("Select role numbers separated by commas, or Enter for none: ")).trim();
|
|
72
|
-
if (!answer)
|
|
73
|
-
return [];
|
|
74
|
-
const indexes = answer.split(",").map((value) => Number(value.trim()) - 1);
|
|
75
|
-
if (indexes.every((index) => Number.isInteger(index) && index >= 0 && index < catalog.roles.length)) {
|
|
76
|
-
return [...new Set(indexes.map((index) => catalog.roles[index].id))];
|
|
77
|
-
}
|
|
78
|
-
console.log("Enter only the listed role numbers.");
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
export async function runCliOnboarding() {
|
|
82
|
-
if (process.env.PI_OFFLINE || process.env.SP_OFFLINE)
|
|
83
|
-
throw new Error("Onboarding requires network access.");
|
|
84
|
-
const state = readSpGatewayState();
|
|
85
|
-
const apiKey = readSpGatewayApiKey();
|
|
86
|
-
if (!state || !apiKey)
|
|
87
|
-
throw new Error("sp-gateway is not configured. Run `sp login` first.");
|
|
88
|
-
const catalog = await fetchSkillCatalog(state.baseUrl, apiKey);
|
|
89
|
-
const prompt = createInterface({ input, output });
|
|
90
|
-
let roles;
|
|
91
|
-
let includeDefaults;
|
|
92
|
-
let includeRoleSkills;
|
|
93
|
-
try {
|
|
94
|
-
roles = await promptRoles(prompt, catalog);
|
|
95
|
-
includeDefaults = await promptYesNo(prompt, "Install the default workflow skills?");
|
|
96
|
-
includeRoleSkills = roles.length > 0 ? await promptYesNo(prompt, "Install role-recommended skills?") : false;
|
|
97
|
-
}
|
|
98
|
-
finally {
|
|
99
|
-
prompt.close();
|
|
100
|
-
}
|
|
101
|
-
const skills = selectCatalogSkills(catalog, roles, includeDefaults, includeRoleSkills);
|
|
102
|
-
console.log(`Installing ${skills.length} selected skill${skills.length === 1 ? "" : "s"}…`);
|
|
103
|
-
const installation = await installOnboardingSelection(getSpAgentDir(), roles, skills);
|
|
104
|
-
console.log(`Installed: ${installation.installedSkillIds.join(", ") || "none"}.`);
|
|
105
|
-
console.log(`Failed: ${installation.failedSkillIds.join(", ") || "none"}.`);
|
|
106
|
-
return installation;
|
|
107
|
-
}
|
|
108
53
|
export async function handleSpCommand(args) {
|
|
109
54
|
const [command, ...rest] = args;
|
|
110
|
-
if (command !== "login" && command !== "logout"
|
|
55
|
+
if (command !== "login" && command !== "logout")
|
|
111
56
|
return false;
|
|
112
57
|
try {
|
|
113
|
-
if (command === "onboarding") {
|
|
114
|
-
if (rest.some((argument) => argument === "-h" || argument === "--help")) {
|
|
115
|
-
printHelp();
|
|
116
|
-
return true;
|
|
117
|
-
}
|
|
118
|
-
if (rest.length > 0)
|
|
119
|
-
throw new Error(`Unknown onboarding option: ${rest[0]}`);
|
|
120
|
-
if (!input.isTTY || !output.isTTY)
|
|
121
|
-
throw new Error("Onboarding requires an interactive terminal.");
|
|
122
|
-
await runCliOnboarding();
|
|
123
|
-
return true;
|
|
124
|
-
}
|
|
125
58
|
if (command === "logout") {
|
|
126
59
|
if (rest.some((argument) => argument === "-h" || argument === "--help")) {
|
|
127
60
|
printHelp();
|
|
@@ -130,7 +63,7 @@ export async function handleSpCommand(args) {
|
|
|
130
63
|
if (rest.length > 0)
|
|
131
64
|
throw new Error(`Unknown logout option: ${rest[0]}`);
|
|
132
65
|
removeSpGatewayLogin();
|
|
133
|
-
console.log("Removed stored
|
|
66
|
+
console.log("Removed stored Senpiper Gateway API key.");
|
|
134
67
|
return true;
|
|
135
68
|
}
|
|
136
69
|
const options = parseLoginOptions(rest);
|
|
@@ -143,21 +76,9 @@ export async function handleSpCommand(args) {
|
|
|
143
76
|
const models = await fetchSpGatewayModels(baseUrl, apiKey);
|
|
144
77
|
writeSpGatewayState(baseUrl, models);
|
|
145
78
|
writeSpGatewayLogin(apiKey, models[0].id);
|
|
146
|
-
console.log(`Connected to
|
|
79
|
+
console.log(`Connected to Senpiper Gateway at ${baseUrl}`);
|
|
147
80
|
console.log(`Synced ${models.length} model${models.length === 1 ? "" : "s"}.`);
|
|
148
81
|
console.log(`Selected model: ${models[0].id}`);
|
|
149
|
-
if (!hasCompletedOnboarding(getSpAgentDir()) && input.isTTY && output.isTTY) {
|
|
150
|
-
try {
|
|
151
|
-
await runCliOnboarding();
|
|
152
|
-
}
|
|
153
|
-
catch (error) {
|
|
154
|
-
console.error(`Onboarding deferred: ${error instanceof Error ? error.message : String(error)}`);
|
|
155
|
-
console.log("Run `sp onboarding` later.");
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
else if (!hasCompletedOnboarding(getSpAgentDir())) {
|
|
159
|
-
console.log("Run `sp onboarding` from an interactive terminal to install curated skills.");
|
|
160
|
-
}
|
|
161
82
|
return true;
|
|
162
83
|
}
|
|
163
84
|
catch (error) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli-commands.js","sourceRoot":"","sources":["../src/cli-commands.ts"],"names":[],"mappings":"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","sourcesContent":["import { stdin as input, stdout as output } from \"node:process\";\nimport { createInterface, type Interface } from \"node:readline/promises\";\nimport {\n\tfetchSpGatewayModels,\n\tgetSpAgentDir,\n\tnormalizeSpGatewayBaseUrl,\n\treadSpGatewayApiKey,\n\treadSpGatewayState,\n\tremoveSpGatewayLogin,\n\twriteSpGatewayLogin,\n\twriteSpGatewayState,\n} from \"./gateway.ts\";\nimport { fetchSkillCatalog, hasCompletedOnboarding, type SkillCatalog, selectCatalogSkills } from \"./onboarding.ts\";\nimport { installOnboardingSelection } from \"./onboarding-installation.ts\";\n\ninterface LoginOptions {\n\tgatewayUrl?: string;\n\tapiKey?: string;\n\thelp?: boolean;\n}\n\nfunction printHelp() {\n\tconsole.log(`sp-agent gateway setup\n\nUsage:\n sp login [--gateway-url <url>] [--api-key <key>]\n sp logout\n sp onboarding\n`);\n}\n\nexport function parseLoginOptions(args: string[]) {\n\tconst options: LoginOptions = {};\n\tfor (let index = 0; index < args.length; index++) {\n\t\tconst argument = args[index];\n\t\tif (argument === \"-h\" || argument === \"--help\") {\n\t\t\toptions.help = true;\n\t\t\tcontinue;\n\t\t}\n\t\tif (argument === \"--gateway-url\") {\n\t\t\toptions.gatewayUrl = args[++index];\n\t\t\tif (!options.gatewayUrl) throw new Error(\"--gateway-url requires a value\");\n\t\t\tcontinue;\n\t\t}\n\t\tif (argument === \"--api-key\") {\n\t\t\toptions.apiKey = args[++index];\n\t\t\tif (!options.apiKey) throw new Error(\"--api-key requires a value\");\n\t\t\tcontinue;\n\t\t}\n\t\tthrow new Error(`Unknown login option: ${argument}`);\n\t}\n\treturn options;\n}\n\nasync function promptForLogin(options: LoginOptions) {\n\tif (options.gatewayUrl && options.apiKey) return { gatewayUrl: options.gatewayUrl, apiKey: options.apiKey };\n\tif (!input.isTTY || !output.isTTY) {\n\t\tthrow new Error(\"Missing gateway credentials. Pass --gateway-url and --api-key in non-interactive mode.\");\n\t}\n\tconst prompt = createInterface({ input, output });\n\ttry {\n\t\treturn {\n\t\t\tgatewayUrl: options.gatewayUrl ?? (await prompt.question(\"sp-gateway URL: \")),\n\t\t\tapiKey: options.apiKey ?? (await prompt.question(\"sp-gateway API key: \")),\n\t\t};\n\t} finally {\n\t\tprompt.close();\n\t}\n}\n\nasync function promptYesNo(prompt: Interface, question: string) {\n\twhile (true) {\n\t\tconst answer = (await prompt.question(`${question} [Y/n] `)).trim().toLowerCase();\n\t\tif (!answer || answer === \"y\" || answer === \"yes\") return true;\n\t\tif (answer === \"n\" || answer === \"no\") return false;\n\t\tconsole.log(\"Enter y or n.\");\n\t}\n}\n\nasync function promptRoles(prompt: Interface, catalog: SkillCatalog) {\n\tconsole.log(\"\\nDeveloper roles\");\n\tfor (const [index, role] of catalog.roles.entries()) console.log(` ${index + 1}. ${role.title}`);\n\twhile (true) {\n\t\tconst answer = (await prompt.question(\"Select role numbers separated by commas, or Enter for none: \")).trim();\n\t\tif (!answer) return [];\n\t\tconst indexes = answer.split(\",\").map((value) => Number(value.trim()) - 1);\n\t\tif (indexes.every((index) => Number.isInteger(index) && index >= 0 && index < catalog.roles.length)) {\n\t\t\treturn [...new Set(indexes.map((index) => catalog.roles[index]!.id))];\n\t\t}\n\t\tconsole.log(\"Enter only the listed role numbers.\");\n\t}\n}\n\nexport async function runCliOnboarding() {\n\tif (process.env.PI_OFFLINE || process.env.SP_OFFLINE) throw new Error(\"Onboarding requires network access.\");\n\tconst state = readSpGatewayState();\n\tconst apiKey = readSpGatewayApiKey();\n\tif (!state || !apiKey) throw new Error(\"sp-gateway is not configured. Run `sp login` first.\");\n\tconst catalog = await fetchSkillCatalog(state.baseUrl, apiKey);\n\tconst prompt = createInterface({ input, output });\n\tlet roles: string[];\n\tlet includeDefaults: boolean;\n\tlet includeRoleSkills: boolean;\n\ttry {\n\t\troles = await promptRoles(prompt, catalog);\n\t\tincludeDefaults = await promptYesNo(prompt, \"Install the default workflow skills?\");\n\t\tincludeRoleSkills = roles.length > 0 ? await promptYesNo(prompt, \"Install role-recommended skills?\") : false;\n\t} finally {\n\t\tprompt.close();\n\t}\n\tconst skills = selectCatalogSkills(catalog, roles, includeDefaults, includeRoleSkills);\n\tconsole.log(`Installing ${skills.length} selected skill${skills.length === 1 ? \"\" : \"s\"}…`);\n\tconst installation = await installOnboardingSelection(getSpAgentDir(), roles, skills);\n\tconsole.log(`Installed: ${installation.installedSkillIds.join(\", \") || \"none\"}.`);\n\tconsole.log(`Failed: ${installation.failedSkillIds.join(\", \") || \"none\"}.`);\n\treturn installation;\n}\n\nexport async function handleSpCommand(args: string[]) {\n\tconst [command, ...rest] = args;\n\tif (command !== \"login\" && command !== \"logout\" && command !== \"onboarding\") return false;\n\ttry {\n\t\tif (command === \"onboarding\") {\n\t\t\tif (rest.some((argument) => argument === \"-h\" || argument === \"--help\")) {\n\t\t\t\tprintHelp();\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tif (rest.length > 0) throw new Error(`Unknown onboarding option: ${rest[0]}`);\n\t\t\tif (!input.isTTY || !output.isTTY) throw new Error(\"Onboarding requires an interactive terminal.\");\n\t\t\tawait runCliOnboarding();\n\t\t\treturn true;\n\t\t}\n\t\tif (command === \"logout\") {\n\t\t\tif (rest.some((argument) => argument === \"-h\" || argument === \"--help\")) {\n\t\t\t\tprintHelp();\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tif (rest.length > 0) throw new Error(`Unknown logout option: ${rest[0]}`);\n\t\t\tremoveSpGatewayLogin();\n\t\t\tconsole.log(\"Removed stored sp-gateway API key.\");\n\t\t\treturn true;\n\t\t}\n\n\t\tconst options = parseLoginOptions(rest);\n\t\tif (options.help) {\n\t\t\tprintHelp();\n\t\t\treturn true;\n\t\t}\n\t\tconst { gatewayUrl, apiKey } = await promptForLogin(options);\n\t\tconst baseUrl = normalizeSpGatewayBaseUrl(gatewayUrl);\n\t\tconst models = await fetchSpGatewayModels(baseUrl, apiKey);\n\t\twriteSpGatewayState(baseUrl, models);\n\t\twriteSpGatewayLogin(apiKey, models[0]!.id);\n\t\tconsole.log(`Connected to sp-gateway at ${baseUrl}`);\n\t\tconsole.log(`Synced ${models.length} model${models.length === 1 ? \"\" : \"s\"}.`);\n\t\tconsole.log(`Selected model: ${models[0]!.id}`);\n\t\tif (!hasCompletedOnboarding(getSpAgentDir()) && input.isTTY && output.isTTY) {\n\t\t\ttry {\n\t\t\t\tawait runCliOnboarding();\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(`Onboarding deferred: ${error instanceof Error ? error.message : String(error)}`);\n\t\t\t\tconsole.log(\"Run `sp onboarding` later.\");\n\t\t\t}\n\t\t} else if (!hasCompletedOnboarding(getSpAgentDir())) {\n\t\t\tconsole.log(\"Run `sp onboarding` from an interactive terminal to install curated skills.\");\n\t\t}\n\t\treturn true;\n\t} catch (error) {\n\t\tconsole.error(`Error: ${error instanceof Error ? error.message : String(error)}`);\n\t\tprocess.exitCode = 1;\n\t\treturn true;\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"cli-commands.js","sourceRoot":"","sources":["../src/cli-commands.ts"],"names":[],"mappings":"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","sourcesContent":["import { stdin as input, stdout as output } from \"node:process\";\nimport { createInterface } from \"node:readline/promises\";\nimport {\n\tfetchSpGatewayModels,\n\tnormalizeSpGatewayBaseUrl,\n\tremoveSpGatewayLogin,\n\twriteSpGatewayLogin,\n\twriteSpGatewayState,\n} from \"./gateway.ts\";\n\ninterface LoginOptions {\n\tgatewayUrl?: string;\n\tapiKey?: string;\n\thelp?: boolean;\n}\n\nfunction printHelp() {\n\tconsole.log(`Senpiper Workbench gateway setup\n\nUsage:\n sp login [--gateway-url <url>] [--api-key <key>]\n sp logout\n`);\n}\n\nexport function parseLoginOptions(args: string[]) {\n\tconst options: LoginOptions = {};\n\tfor (let index = 0; index < args.length; index++) {\n\t\tconst argument = args[index];\n\t\tif (argument === \"-h\" || argument === \"--help\") {\n\t\t\toptions.help = true;\n\t\t\tcontinue;\n\t\t}\n\t\tif (argument === \"--gateway-url\") {\n\t\t\toptions.gatewayUrl = args[++index];\n\t\t\tif (!options.gatewayUrl) throw new Error(\"--gateway-url requires a value\");\n\t\t\tcontinue;\n\t\t}\n\t\tif (argument === \"--api-key\") {\n\t\t\toptions.apiKey = args[++index];\n\t\t\tif (!options.apiKey) throw new Error(\"--api-key requires a value\");\n\t\t\tcontinue;\n\t\t}\n\t\tthrow new Error(`Unknown login option: ${argument}`);\n\t}\n\treturn options;\n}\n\nasync function promptForLogin(options: LoginOptions) {\n\tif (options.gatewayUrl && options.apiKey) return { gatewayUrl: options.gatewayUrl, apiKey: options.apiKey };\n\tif (!input.isTTY || !output.isTTY) {\n\t\tthrow new Error(\"Missing gateway credentials. Pass --gateway-url and --api-key in non-interactive mode.\");\n\t}\n\tconst prompt = createInterface({ input, output });\n\ttry {\n\t\treturn {\n\t\t\tgatewayUrl: options.gatewayUrl ?? (await prompt.question(\"Senpiper Gateway URL: \")),\n\t\t\tapiKey: options.apiKey ?? (await prompt.question(\"Senpiper Gateway API key: \")),\n\t\t};\n\t} finally {\n\t\tprompt.close();\n\t}\n}\n\nexport async function handleSpCommand(args: string[]) {\n\tconst [command, ...rest] = args;\n\tif (command !== \"login\" && command !== \"logout\") return false;\n\ttry {\n\t\tif (command === \"logout\") {\n\t\t\tif (rest.some((argument) => argument === \"-h\" || argument === \"--help\")) {\n\t\t\t\tprintHelp();\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tif (rest.length > 0) throw new Error(`Unknown logout option: ${rest[0]}`);\n\t\t\tremoveSpGatewayLogin();\n\t\t\tconsole.log(\"Removed stored Senpiper Gateway API key.\");\n\t\t\treturn true;\n\t\t}\n\n\t\tconst options = parseLoginOptions(rest);\n\t\tif (options.help) {\n\t\t\tprintHelp();\n\t\t\treturn true;\n\t\t}\n\t\tconst { gatewayUrl, apiKey } = await promptForLogin(options);\n\t\tconst baseUrl = normalizeSpGatewayBaseUrl(gatewayUrl);\n\t\tconst models = await fetchSpGatewayModels(baseUrl, apiKey);\n\t\twriteSpGatewayState(baseUrl, models);\n\t\twriteSpGatewayLogin(apiKey, models[0]!.id);\n\t\tconsole.log(`Connected to Senpiper Gateway at ${baseUrl}`);\n\t\tconsole.log(`Synced ${models.length} model${models.length === 1 ? \"\" : \"s\"}.`);\n\t\tconsole.log(`Selected model: ${models[0]!.id}`);\n\t\treturn true;\n\t} catch (error) {\n\t\tconsole.error(`Error: ${error instanceof Error ? error.message : String(error)}`);\n\t\tprocess.exitCode = 1;\n\t\treturn true;\n\t}\n}\n"]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@senpiper/pi-coding-agent";
|
|
2
|
-
import { handleSpCommand
|
|
2
|
+
import { handleSpCommand } from "./cli-commands.ts";
|
|
3
3
|
export type SpCommandExtensionApi = ExtensionAPI;
|
|
4
|
-
export { handleSpCommand
|
|
4
|
+
export { handleSpCommand };
|
|
5
5
|
export declare function registerSpCommands(pi: ExtensionAPI): void;
|
|
6
6
|
export declare function getSpGatewayStatus(): import("./gateway.ts").SpGatewayState | undefined;
|
|
7
7
|
//# sourceMappingURL=commands.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAAE,eAAe,EAAE,
|
|
1
|
+
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAIpD,MAAM,MAAM,qBAAqB,GAAG,YAAY,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,CAAC;AAE3B,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,YAAY,QAElD;AAED,wBAAgB,kBAAkB,sDAEjC","sourcesContent":["import type { ExtensionAPI } from \"@senpiper/pi-coding-agent\";\nimport { handleSpCommand } from \"./cli-commands.ts\";\nimport { readSpGatewayState } from \"./gateway.ts\";\nimport { registerLoginCommand } from \"./login-command.ts\";\n\nexport type SpCommandExtensionApi = ExtensionAPI;\nexport { handleSpCommand };\n\nexport function registerSpCommands(pi: ExtensionAPI) {\n\tregisterLoginCommand(pi);\n}\n\nexport function getSpGatewayStatus() {\n\treturn readSpGatewayState();\n}\n"]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { handleSpCommand
|
|
1
|
+
import { handleSpCommand } from "./cli-commands.js";
|
|
2
2
|
import { readSpGatewayState } from "./gateway.js";
|
|
3
3
|
import { registerLoginCommand } from "./login-command.js";
|
|
4
|
-
export { handleSpCommand
|
|
4
|
+
export { handleSpCommand };
|
|
5
5
|
export function registerSpCommands(pi) {
|
|
6
6
|
registerLoginCommand(pi);
|
|
7
7
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"commands.js","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,
|
|
1
|
+
{"version":3,"file":"commands.js","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAG1D,OAAO,EAAE,eAAe,EAAE,CAAC;AAE3B,MAAM,UAAU,kBAAkB,CAAC,EAAgB,EAAE;IACpD,oBAAoB,CAAC,EAAE,CAAC,CAAC;AAAA,CACzB;AAED,MAAM,UAAU,kBAAkB,GAAG;IACpC,OAAO,kBAAkB,EAAE,CAAC;AAAA,CAC5B","sourcesContent":["import type { ExtensionAPI } from \"@senpiper/pi-coding-agent\";\nimport { handleSpCommand } from \"./cli-commands.ts\";\nimport { readSpGatewayState } from \"./gateway.ts\";\nimport { registerLoginCommand } from \"./login-command.ts\";\n\nexport type SpCommandExtensionApi = ExtensionAPI;\nexport { handleSpCommand };\n\nexport function registerSpCommands(pi: ExtensionAPI) {\n\tregisterLoginCommand(pi);\n}\n\nexport function getSpGatewayStatus() {\n\treturn readSpGatewayState();\n}\n"]}
|
|
@@ -3,7 +3,7 @@ export type SpExtensionApi = ExtensionAPI;
|
|
|
3
3
|
export type SpProviderModelConfig = ProviderModelConfig;
|
|
4
4
|
export declare const SP_GATEWAY_PROVIDER_ID = "sp-gateway";
|
|
5
5
|
export declare const LEGACY_SP_GATEWAY_PROVIDER_ID = "sengateway";
|
|
6
|
-
export declare const SP_GATEWAY_PROVIDER_NAME = "
|
|
6
|
+
export declare const SP_GATEWAY_PROVIDER_NAME = "Senpiper Gateway";
|
|
7
7
|
export declare const SP_USER_AGENT = "sp";
|
|
8
8
|
export declare const SP_CLIENT_NAME = "sp-agent";
|
|
9
9
|
export interface SpGatewayState {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../src/gateway.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,gBAAgB,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAGrH,MAAM,MAAM,cAAc,GAAG,YAAY,CAAC;AAC1C,MAAM,MAAM,qBAAqB,GAAG,mBAAmB,CAAC;AAExD,eAAO,MAAM,sBAAsB,eAAe,CAAC;AACnD,eAAO,MAAM,6BAA6B,eAAe,CAAC;AAC1D,eAAO,MAAM,wBAAwB,eAAe,CAAC;AACrD,eAAO,MAAM,aAAa,OAAO,CAAC;AAClC,eAAO,MAAM,cAAc,aAAa,CAAC;AAEzC,MAAM,WAAW,cAAc;IAC9B,aAAa,EAAE,CAAC,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,qBAAqB,EAAE,CAAC;CAChC;AAED,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,gBAAgB,GAAG,uBAAuB,CAAC;IACjD,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,EAAE,MAAM,CAAC;CACX;AAgCD,wBAAgB,aAAa,WAE5B;AAED,wBAAgB,qBAAqB,CAAC,QAAQ,SAAkB,UAE/D;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,SAAkB,UAE7D;AAED,wBAAgB,aAAa,CAAC,QAAQ,SAAkB,UAEvD;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,SAAkB,UAE3D;AAoBD,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,MAAM,UAkBtD;AA4GD,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,kCAoB/F;AAiBD,wBAAgB,kBAAkB,CAAC,QAAQ,SAAkB,GAAG,cAAc,GAAG,SAAS,CAsBzF;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,qBAAqB,EAAE,EAAE,QAAQ,SAAkB,QAO/G;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,SAAkB,sBAa7D;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,QAAQ,SAAkB,QAYnG;AAED,wBAAgB,oBAAoB,CAAC,QAAQ,SAAkB,QAM9D;AAeD,wBAAgB,mBAAmB,iCAElC;AAED,wBAAgB,qBAAqB,SAEpC;AAED,wBAAgB,6BAA6B,CAC5C,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,qBAAqB,EAAE,EAC/B,QAAQ,SAAkB,GACxB,cAAc,CAsChB;AAED,wBAAgB,wBAAwB,IAAI,gBAAgB,CAyB3D","sourcesContent":["import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport type { ExtensionAPI, ExtensionFactory, ProviderConfig, ProviderModelConfig } from \"@senpiper/pi-coding-agent\";\nimport type { Api, Model, OpenAICompletionsCompat, RefreshModelsContext } from \"@senpiper/pi-coding-agent/runtime-deps\";\n\nexport type SpExtensionApi = ExtensionAPI;\nexport type SpProviderModelConfig = ProviderModelConfig;\n\nexport const SP_GATEWAY_PROVIDER_ID = \"sp-gateway\";\nexport const LEGACY_SP_GATEWAY_PROVIDER_ID = \"sengateway\";\nexport const SP_GATEWAY_PROVIDER_NAME = \"sp-gateway\";\nexport const SP_USER_AGENT = \"sp\";\nexport const SP_CLIENT_NAME = \"sp-agent\";\n\nexport interface SpGatewayState {\n\tschemaVersion: 1;\n\tbaseUrl: string;\n\tmodels: SpProviderModelConfig[];\n}\n\nexport interface SpGatewayWarning {\n\tkind: \"refresh_failed\" | \"legacy_cache_imported\";\n\tmessage: string;\n\tat: string;\n}\n\ntype JsonRecord = Record<string, unknown>;\n\nlet gatewayWarning: SpGatewayWarning | undefined;\n\nfunction isRecord(value: unknown): value is JsonRecord {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction stringValue(value: unknown) {\n\treturn typeof value === \"string\" && value.trim() ? value.trim() : undefined;\n}\n\nfunction finiteNumber(value: unknown) {\n\treturn typeof value === \"number\" && Number.isFinite(value) ? value : undefined;\n}\n\nfunction positiveInteger(value: unknown) {\n\tconst number = finiteNumber(value);\n\treturn number !== undefined && number > 0 ? Math.floor(number) : undefined;\n}\n\nfunction nonNegativeNumber(value: unknown) {\n\tconst number = finiteNumber(value);\n\treturn number !== undefined && number >= 0 ? number : undefined;\n}\n\nfunction booleanValue(value: unknown) {\n\treturn typeof value === \"boolean\" ? value : undefined;\n}\n\nexport function getSpAgentDir() {\n\treturn process.env.SP_CODING_AGENT_DIR || join(homedir(), \".sp\", \"agent\");\n}\n\nexport function getSpGatewayStatePath(agentDir = getSpAgentDir()) {\n\treturn join(agentDir, \"sp-gateway.json\");\n}\n\nexport function getLegacyModelsPath(agentDir = getSpAgentDir()) {\n\treturn join(agentDir, \"models.json\");\n}\n\nexport function getSpAuthPath(agentDir = getSpAgentDir()) {\n\treturn join(agentDir, \"auth.json\");\n}\n\nexport function getSpSettingsPath(agentDir = getSpAgentDir()) {\n\treturn join(agentDir, \"settings.json\");\n}\n\nfunction readJsonFile(path: string): JsonRecord {\n\tif (!existsSync(path)) return {};\n\tconst content = readFileSync(path, \"utf8\").trim();\n\tif (!content) return {};\n\tconst parsed = JSON.parse(content) as unknown;\n\tif (!isRecord(parsed)) throw new Error(`Expected an object in ${path}`);\n\treturn parsed;\n}\n\nfunction writePrivateJson(path: string, value: unknown) {\n\tmkdirSync(dirname(path), { recursive: true });\n\tconst temporaryPath = `${path}.${process.pid}.tmp`;\n\twriteFileSync(temporaryPath, `${JSON.stringify(value, null, \"\\t\")}\\n`, { encoding: \"utf8\", mode: 0o600 });\n\tif (process.platform !== \"win32\") chmodSync(temporaryPath, 0o600);\n\trenameSync(temporaryPath, path);\n\tif (process.platform !== \"win32\") chmodSync(path, 0o600);\n}\n\nexport function normalizeSpGatewayBaseUrl(input: string) {\n\tconst trimmed = input.trim();\n\tif (!trimmed) throw new Error(\"Gateway URL cannot be empty.\");\n\tconst withScheme = /^[a-z][a-z0-9+.-]*:\\/\\//iu.test(trimmed) ? trimmed : `https://${trimmed}`;\n\tlet url: URL;\n\ttry {\n\t\turl = new URL(withScheme);\n\t} catch {\n\t\tthrow new Error(`Invalid gateway URL: ${input}`);\n\t}\n\tif (url.protocol !== \"https:\" && url.protocol !== \"http:\") {\n\t\tthrow new Error(\"Gateway URL must use HTTPS or HTTP.\");\n\t}\n\turl.hash = \"\";\n\turl.search = \"\";\n\turl.pathname = url.pathname.replace(/\\/+$/u, \"\");\n\tif (!url.pathname.endsWith(\"/v1\")) url.pathname = `${url.pathname}/v1`.replace(/\\/+/gu, \"/\");\n\treturn url.toString().replace(/\\/$/u, \"\");\n}\n\nfunction parseInput(value: unknown): (\"text\" | \"image\")[] | undefined {\n\tif (!Array.isArray(value)) return undefined;\n\tconst input = value.filter((entry): entry is \"text\" | \"image\" => entry === \"text\" || entry === \"image\");\n\treturn input.length > 0 ? [...new Set(input)] : undefined;\n}\n\nfunction parseCost(value: unknown): SpProviderModelConfig[\"cost\"] | undefined {\n\tif (!isRecord(value)) return undefined;\n\tconst input = nonNegativeNumber(value.input);\n\tconst output = nonNegativeNumber(value.output);\n\tconst cacheRead = nonNegativeNumber(value.cacheRead);\n\tconst cacheWrite = nonNegativeNumber(value.cacheWrite);\n\tif (input === undefined && output === undefined && cacheRead === undefined && cacheWrite === undefined)\n\t\treturn undefined;\n\treturn { input: input ?? 0, output: output ?? 0, cacheRead: cacheRead ?? 0, cacheWrite: cacheWrite ?? 0 };\n}\n\nfunction parseThinkingLevelMap(value: unknown): SpProviderModelConfig[\"thinkingLevelMap\"] {\n\tif (!isRecord(value)) return undefined;\n\tconst result: NonNullable<SpProviderModelConfig[\"thinkingLevelMap\"]> = {};\n\tfor (const key of [\"off\", \"minimal\", \"low\", \"medium\", \"high\", \"xhigh\", \"max\"] as const) {\n\t\tconst entry = value[key];\n\t\tif (typeof entry === \"string\" || entry === null) result[key] = entry;\n\t}\n\treturn Object.keys(result).length > 0 ? result : undefined;\n}\n\nfunction thinkingFormat(value: unknown): OpenAICompletionsCompat[\"thinkingFormat\"] {\n\tswitch (value) {\n\t\tcase \"openai\":\n\t\tcase \"openrouter\":\n\t\tcase \"deepseek\":\n\t\tcase \"together\":\n\t\tcase \"zai\":\n\t\tcase \"qwen\":\n\t\tcase \"chat-template\":\n\t\tcase \"qwen-chat-template\":\n\t\tcase \"string-thinking\":\n\t\tcase \"ant-ling\":\n\t\t\treturn value;\n\t\tdefault:\n\t\t\treturn undefined;\n\t}\n}\n\nfunction sessionAffinityFormat(value: unknown): OpenAICompletionsCompat[\"sessionAffinityFormat\"] {\n\treturn value === \"openai\" || value === \"openai-nosession\" || value === \"openrouter\" ? value : undefined;\n}\n\nfunction parseCompat(value: unknown): OpenAICompletionsCompat {\n\tconst record = isRecord(value) ? value : {};\n\tconst maxTokensField =\n\t\trecord.maxTokensField === \"max_completion_tokens\" || record.maxTokensField === \"max_tokens\"\n\t\t\t? record.maxTokensField\n\t\t\t: undefined;\n\tconst compat = {\n\t\tsupportsStore: booleanValue(record.supportsStore) ?? false,\n\t\tsupportsDeveloperRole: booleanValue(record.supportsDeveloperRole),\n\t\tsupportsReasoningEffort: booleanValue(record.supportsReasoningEffort),\n\t\tsupportsUsageInStreaming: booleanValue(record.supportsUsageInStreaming),\n\t\tmaxTokensField,\n\t\trequiresToolResultName: booleanValue(record.requiresToolResultName),\n\t\trequiresAssistantAfterToolResult: booleanValue(record.requiresAssistantAfterToolResult),\n\t\trequiresThinkingAsText: booleanValue(record.requiresThinkingAsText),\n\t\trequiresReasoningContentOnAssistantMessages: booleanValue(record.requiresReasoningContentOnAssistantMessages),\n\t\tthinkingFormat: thinkingFormat(record.thinkingFormat),\n\t\tsupportsStrictMode: booleanValue(record.supportsStrictMode),\n\t\tsendSessionAffinityHeaders: booleanValue(record.sendSessionAffinityHeaders),\n\t\tsessionAffinityFormat: sessionAffinityFormat(record.sessionAffinityFormat),\n\t\tsupportsLongCacheRetention: booleanValue(record.supportsLongCacheRetention),\n\t} satisfies OpenAICompletionsCompat;\n\treturn compat;\n}\n\nfunction parseGatewayModel(value: unknown): SpProviderModelConfig | undefined {\n\tconst record = typeof value === \"string\" ? { id: value } : isRecord(value) ? value : undefined;\n\tif (!record) return undefined;\n\tconst id = stringValue(record.id);\n\tif (!id || id.length > 256 || /[\\u0000-\\u001f\\u007f]/u.test(id)) return undefined;\n\tconst metadata = isRecord(record.metadata) ? record.metadata : {};\n\tconst field = (name: string) => metadata[name] ?? record[name];\n\tconst reasoning = field(\"reasoning\");\n\treturn {\n\t\tid,\n\t\tname: stringValue(field(\"name\")) ?? id,\n\t\treasoning: typeof reasoning === \"boolean\" ? reasoning : false,\n\t\tthinkingLevelMap: parseThinkingLevelMap(field(\"thinkingLevelMap\")),\n\t\tinput: parseInput(field(\"input\")) ?? [\"text\"],\n\t\tcost: parseCost(field(\"cost\")) ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\t\tcontextWindow: positiveInteger(field(\"contextWindow\")) ?? 128_000,\n\t\tmaxTokens: positiveInteger(field(\"maxTokens\")) ?? 32_000,\n\t\tcompat: parseCompat(field(\"compat\")),\n\t};\n}\n\nfunction parseModelCatalog(values: readonly unknown[], source: string) {\n\tconst models = values.map(parseGatewayModel).filter((model): model is SpProviderModelConfig => model !== undefined);\n\tif (models.length === 0) throw new Error(`${source} contains no valid models.`);\n\tconst ids = new Set<string>();\n\tfor (const model of models) {\n\t\tif (ids.has(model.id)) throw new Error(`${source} contains duplicate model id ${model.id}.`);\n\t\tids.add(model.id);\n\t}\n\treturn models;\n}\n\nexport async function fetchSpGatewayModels(baseUrl: string, apiKey: string, signal?: AbortSignal) {\n\tconst response = await fetch(`${baseUrl.replace(/\\/$/u, \"\")}/models`, {\n\t\tsignal,\n\t\theaders: {\n\t\t\tAuthorization: `Bearer ${apiKey}`,\n\t\t\tAccept: \"application/json\",\n\t\t\t\"User-Agent\": SP_USER_AGENT,\n\t\t\t\"X-Senpiper-Client\": SP_CLIENT_NAME,\n\t\t},\n\t});\n\tif (!response.ok) {\n\t\tconst body = await response.text().catch(() => \"\");\n\t\tconst suffix = body.trim() ? `: ${body.trim().slice(0, 300)}` : \"\";\n\t\tthrow new Error(`sp-gateway rejected /v1/models (${response.status} ${response.statusText})${suffix}`);\n\t}\n\tconst payload = (await response.json()) as unknown;\n\tif (!isRecord(payload) || !Array.isArray(payload.data)) {\n\t\tthrow new Error(\"sp-gateway returned an invalid /v1/models envelope.\");\n\t}\n\treturn parseModelCatalog(payload.data, \"sp-gateway /v1/models response\");\n}\n\nfunction readLegacySpGatewayState(agentDir: string): SpGatewayState | undefined {\n\tconst path = getLegacyModelsPath(agentDir);\n\tif (!existsSync(path)) return undefined;\n\tconst root = readJsonFile(path);\n\tconst providers = isRecord(root.providers) ? root.providers : undefined;\n\tconst providerValue = providers?.[SP_GATEWAY_PROVIDER_ID] ?? providers?.[LEGACY_SP_GATEWAY_PROVIDER_ID];\n\tconst provider = isRecord(providerValue) ? providerValue : undefined;\n\tif (!provider || typeof provider.baseUrl !== \"string\" || !Array.isArray(provider.models)) return undefined;\n\treturn {\n\t\tschemaVersion: 1,\n\t\tbaseUrl: normalizeSpGatewayBaseUrl(provider.baseUrl),\n\t\tmodels: parseModelCatalog(provider.models, `legacy sp-gateway cache ${path}`),\n\t};\n}\n\nexport function readSpGatewayState(agentDir = getSpAgentDir()): SpGatewayState | undefined {\n\tconst path = getSpGatewayStatePath(agentDir);\n\tif (!existsSync(path)) {\n\t\tconst legacy = readLegacySpGatewayState(agentDir);\n\t\tif (!legacy) return undefined;\n\t\twriteSpGatewayState(legacy.baseUrl, legacy.models, agentDir);\n\t\tgatewayWarning = {\n\t\t\tkind: \"legacy_cache_imported\",\n\t\t\tmessage: `Imported the legacy sp-gateway model cache from ${getLegacyModelsPath(agentDir)}.`,\n\t\t\tat: new Date().toISOString(),\n\t\t};\n\t\treturn legacy;\n\t}\n\tconst value = readJsonFile(path);\n\tif (value.schemaVersion !== 1 || typeof value.baseUrl !== \"string\" || !Array.isArray(value.models)) {\n\t\tthrow new Error(`Invalid sp-gateway configuration: ${path}`);\n\t}\n\treturn {\n\t\tschemaVersion: 1,\n\t\tbaseUrl: normalizeSpGatewayBaseUrl(value.baseUrl),\n\t\tmodels: parseModelCatalog(value.models, `sp-gateway configuration ${path}`),\n\t};\n}\n\nexport function writeSpGatewayState(baseUrl: string, models: SpProviderModelConfig[], agentDir = getSpAgentDir()) {\n\tconst validatedModels = parseModelCatalog(models, \"sp-gateway state\");\n\twritePrivateJson(getSpGatewayStatePath(agentDir), {\n\t\tschemaVersion: 1,\n\t\tbaseUrl: normalizeSpGatewayBaseUrl(baseUrl),\n\t\tmodels: validatedModels,\n\t} satisfies SpGatewayState);\n}\n\nexport function readSpGatewayApiKey(agentDir = getSpAgentDir()) {\n\tconst path = getSpAuthPath(agentDir);\n\tconst auth = readJsonFile(path);\n\tlet credential = auth[SP_GATEWAY_PROVIDER_ID];\n\tif (!credential && auth[LEGACY_SP_GATEWAY_PROVIDER_ID]) {\n\t\tcredential = auth[LEGACY_SP_GATEWAY_PROVIDER_ID];\n\t\tauth[SP_GATEWAY_PROVIDER_ID] = credential;\n\t\tdelete auth[LEGACY_SP_GATEWAY_PROVIDER_ID];\n\t\twritePrivateJson(path, auth);\n\t}\n\treturn isRecord(credential) && credential.type === \"api_key\" && typeof credential.key === \"string\"\n\t\t? credential.key\n\t\t: undefined;\n}\n\nexport function writeSpGatewayLogin(apiKey: string, firstModelId: string, agentDir = getSpAgentDir()) {\n\tconst authPath = getSpAuthPath(agentDir);\n\tconst auth = readJsonFile(authPath);\n\tauth[SP_GATEWAY_PROVIDER_ID] = { type: \"api_key\", key: apiKey };\n\tdelete auth[LEGACY_SP_GATEWAY_PROVIDER_ID];\n\twritePrivateJson(authPath, auth);\n\n\tconst settingsPath = getSpSettingsPath(agentDir);\n\tconst settings = readJsonFile(settingsPath);\n\tsettings.defaultProvider = SP_GATEWAY_PROVIDER_ID;\n\tsettings.defaultModel = firstModelId;\n\twritePrivateJson(settingsPath, settings);\n}\n\nexport function removeSpGatewayLogin(agentDir = getSpAgentDir()) {\n\tconst path = getSpAuthPath(agentDir);\n\tconst auth = readJsonFile(path);\n\tdelete auth[SP_GATEWAY_PROVIDER_ID];\n\tdelete auth[LEGACY_SP_GATEWAY_PROVIDER_ID];\n\twritePrivateJson(path, auth);\n}\n\nfunction credentialKey(context: RefreshModelsContext) {\n\treturn context.credential?.type === \"api_key\" ? context.credential.key : undefined;\n}\n\nfunction storedModels(models: readonly SpProviderModelConfig[], baseUrl: string): Model<Api>[] {\n\treturn models.map((model) => ({\n\t\t...model,\n\t\tprovider: SP_GATEWAY_PROVIDER_ID,\n\t\tbaseUrl,\n\t\tapi: \"openai-completions\",\n\t}));\n}\n\nexport function getSpGatewayWarning() {\n\treturn gatewayWarning;\n}\n\nexport function clearSpGatewayWarning() {\n\tgatewayWarning = undefined;\n}\n\nexport function createSpGatewayProviderConfig(\n\tbaseUrl: string,\n\tmodels: SpProviderModelConfig[],\n\tagentDir = getSpAgentDir(),\n): ProviderConfig {\n\tlet currentModels = parseModelCatalog(models, \"sp-gateway provider configuration\");\n\treturn {\n\t\tname: SP_GATEWAY_PROVIDER_NAME,\n\t\tbaseUrl,\n\t\tapi: \"openai-completions\",\n\t\tauthHeader: true,\n\t\theaders: {\n\t\t\t\"User-Agent\": SP_USER_AGENT,\n\t\t\t\"X-Senpiper-Client\": SP_CLIENT_NAME,\n\t\t},\n\t\tmodels: currentModels,\n\t\tasync refreshModels(context) {\n\t\t\tif (!context.allowNetwork) return currentModels;\n\t\t\tconst apiKey = credentialKey(context);\n\t\t\tif (!apiKey) return currentModels;\n\t\t\ttry {\n\t\t\t\tconst refreshed = await fetchSpGatewayModels(baseUrl, apiKey, context.signal);\n\t\t\t\tif (context.signal.aborted) return currentModels;\n\t\t\t\tawait context.publish({\n\t\t\t\t\tpersist: { models: storedModels(refreshed, baseUrl), checkedAt: Date.now() },\n\t\t\t\t\tupdate: () => {\n\t\t\t\t\t\twriteSpGatewayState(baseUrl, refreshed, agentDir);\n\t\t\t\t\t\tcurrentModels = refreshed;\n\t\t\t\t\t\tgatewayWarning = undefined;\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t\treturn currentModels;\n\t\t\t} catch (error) {\n\t\t\t\tgatewayWarning = {\n\t\t\t\t\tkind: \"refresh_failed\",\n\t\t\t\t\tmessage: `sp-gateway model refresh failed; using ${currentModels.length} cached model${currentModels.length === 1 ? \"\" : \"s\"}: ${error instanceof Error ? error.message : String(error)}`,\n\t\t\t\t\tat: new Date().toISOString(),\n\t\t\t\t};\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t},\n\t};\n}\n\nexport function createSpGatewayExtension(): ExtensionFactory {\n\treturn (pi) => {\n\t\tconst state = readSpGatewayState();\n\t\tif (state) {\n\t\t\tpi.registerProvider(SP_GATEWAY_PROVIDER_ID, createSpGatewayProviderConfig(state.baseUrl, state.models));\n\t\t}\n\n\t\tpi.on(\"before_agent_start\", (event) => ({\n\t\t\tsystemPrompt: `${event.systemPrompt}\\n\\n<sp_agent_runtime>\\nThis is Senpiper's sp-agent distribution. Use only the configured sp-gateway model provider. Put user-owned extensions, skills, prompts, and themes under the active .sp configuration directory instead of modifying the sp-agent installation.\\n</sp_agent_runtime>`,\n\t\t}));\n\n\t\tpi.on(\"session_start\", (_event, ctx) => {\n\t\t\tctx.ui.setTitle(\"sp-agent\");\n\t\t\tconst warning = getSpGatewayWarning();\n\t\t\tif (warning) ctx.ui.notify(warning.message, \"warning\");\n\t\t\tif (ctx.mode !== \"tui\") return;\n\t\t\tctx.ui.setHeader((_tui, theme) => ({\n\t\t\t\trender: () => [\n\t\t\t\t\ttheme.fg(\"accent\", theme.bold(\"sp-agent\")),\n\t\t\t\t\ttheme.fg(\"muted\", state ? `sp-gateway · ${state.models.length} models` : \"run `sp login`\"),\n\t\t\t\t],\n\t\t\t\tinvalidate() {},\n\t\t\t}));\n\t\t});\n\t};\n}\n"]}
|
|
1
|
+
{"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../src/gateway.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,gBAAgB,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAGrH,MAAM,MAAM,cAAc,GAAG,YAAY,CAAC;AAC1C,MAAM,MAAM,qBAAqB,GAAG,mBAAmB,CAAC;AAExD,eAAO,MAAM,sBAAsB,eAAe,CAAC;AACnD,eAAO,MAAM,6BAA6B,eAAe,CAAC;AAC1D,eAAO,MAAM,wBAAwB,qBAAqB,CAAC;AAC3D,eAAO,MAAM,aAAa,OAAO,CAAC;AAClC,eAAO,MAAM,cAAc,aAAa,CAAC;AAEzC,MAAM,WAAW,cAAc;IAC9B,aAAa,EAAE,CAAC,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,qBAAqB,EAAE,CAAC;CAChC;AAED,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,gBAAgB,GAAG,uBAAuB,CAAC;IACjD,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,EAAE,MAAM,CAAC;CACX;AAgCD,wBAAgB,aAAa,WAE5B;AAED,wBAAgB,qBAAqB,CAAC,QAAQ,SAAkB,UAE/D;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,SAAkB,UAE7D;AAED,wBAAgB,aAAa,CAAC,QAAQ,SAAkB,UAEvD;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,SAAkB,UAE3D;AAoBD,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,MAAM,UAkBtD;AA4GD,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,kCAoB/F;AAiBD,wBAAgB,kBAAkB,CAAC,QAAQ,SAAkB,GAAG,cAAc,GAAG,SAAS,CAsBzF;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,qBAAqB,EAAE,EAAE,QAAQ,SAAkB,QAO/G;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,SAAkB,sBAa7D;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,QAAQ,SAAkB,QAYnG;AAED,wBAAgB,oBAAoB,CAAC,QAAQ,SAAkB,QAM9D;AAeD,wBAAgB,mBAAmB,iCAElC;AAED,wBAAgB,qBAAqB,SAEpC;AAED,wBAAgB,6BAA6B,CAC5C,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,qBAAqB,EAAE,EAC/B,QAAQ,SAAkB,GACxB,cAAc,CAsChB;AAED,wBAAgB,wBAAwB,IAAI,gBAAgB,CAuC3D","sourcesContent":["import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport type { ExtensionAPI, ExtensionFactory, ProviderConfig, ProviderModelConfig } from \"@senpiper/pi-coding-agent\";\nimport type { Api, Model, OpenAICompletionsCompat, RefreshModelsContext } from \"@senpiper/pi-coding-agent/runtime-deps\";\n\nexport type SpExtensionApi = ExtensionAPI;\nexport type SpProviderModelConfig = ProviderModelConfig;\n\nexport const SP_GATEWAY_PROVIDER_ID = \"sp-gateway\";\nexport const LEGACY_SP_GATEWAY_PROVIDER_ID = \"sengateway\";\nexport const SP_GATEWAY_PROVIDER_NAME = \"Senpiper Gateway\";\nexport const SP_USER_AGENT = \"sp\";\nexport const SP_CLIENT_NAME = \"sp-agent\";\n\nexport interface SpGatewayState {\n\tschemaVersion: 1;\n\tbaseUrl: string;\n\tmodels: SpProviderModelConfig[];\n}\n\nexport interface SpGatewayWarning {\n\tkind: \"refresh_failed\" | \"legacy_cache_imported\";\n\tmessage: string;\n\tat: string;\n}\n\ntype JsonRecord = Record<string, unknown>;\n\nlet gatewayWarning: SpGatewayWarning | undefined;\n\nfunction isRecord(value: unknown): value is JsonRecord {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction stringValue(value: unknown) {\n\treturn typeof value === \"string\" && value.trim() ? value.trim() : undefined;\n}\n\nfunction finiteNumber(value: unknown) {\n\treturn typeof value === \"number\" && Number.isFinite(value) ? value : undefined;\n}\n\nfunction positiveInteger(value: unknown) {\n\tconst number = finiteNumber(value);\n\treturn number !== undefined && number > 0 ? Math.floor(number) : undefined;\n}\n\nfunction nonNegativeNumber(value: unknown) {\n\tconst number = finiteNumber(value);\n\treturn number !== undefined && number >= 0 ? number : undefined;\n}\n\nfunction booleanValue(value: unknown) {\n\treturn typeof value === \"boolean\" ? value : undefined;\n}\n\nexport function getSpAgentDir() {\n\treturn process.env.SP_CODING_AGENT_DIR || join(homedir(), \".sp\", \"agent\");\n}\n\nexport function getSpGatewayStatePath(agentDir = getSpAgentDir()) {\n\treturn join(agentDir, \"sp-gateway.json\");\n}\n\nexport function getLegacyModelsPath(agentDir = getSpAgentDir()) {\n\treturn join(agentDir, \"models.json\");\n}\n\nexport function getSpAuthPath(agentDir = getSpAgentDir()) {\n\treturn join(agentDir, \"auth.json\");\n}\n\nexport function getSpSettingsPath(agentDir = getSpAgentDir()) {\n\treturn join(agentDir, \"settings.json\");\n}\n\nfunction readJsonFile(path: string): JsonRecord {\n\tif (!existsSync(path)) return {};\n\tconst content = readFileSync(path, \"utf8\").trim();\n\tif (!content) return {};\n\tconst parsed = JSON.parse(content) as unknown;\n\tif (!isRecord(parsed)) throw new Error(`Expected an object in ${path}`);\n\treturn parsed;\n}\n\nfunction writePrivateJson(path: string, value: unknown) {\n\tmkdirSync(dirname(path), { recursive: true });\n\tconst temporaryPath = `${path}.${process.pid}.tmp`;\n\twriteFileSync(temporaryPath, `${JSON.stringify(value, null, \"\\t\")}\\n`, { encoding: \"utf8\", mode: 0o600 });\n\tif (process.platform !== \"win32\") chmodSync(temporaryPath, 0o600);\n\trenameSync(temporaryPath, path);\n\tif (process.platform !== \"win32\") chmodSync(path, 0o600);\n}\n\nexport function normalizeSpGatewayBaseUrl(input: string) {\n\tconst trimmed = input.trim();\n\tif (!trimmed) throw new Error(\"Gateway URL cannot be empty.\");\n\tconst withScheme = /^[a-z][a-z0-9+.-]*:\\/\\//iu.test(trimmed) ? trimmed : `https://${trimmed}`;\n\tlet url: URL;\n\ttry {\n\t\turl = new URL(withScheme);\n\t} catch {\n\t\tthrow new Error(`Invalid gateway URL: ${input}`);\n\t}\n\tif (url.protocol !== \"https:\" && url.protocol !== \"http:\") {\n\t\tthrow new Error(\"Gateway URL must use HTTPS or HTTP.\");\n\t}\n\turl.hash = \"\";\n\turl.search = \"\";\n\turl.pathname = url.pathname.replace(/\\/+$/u, \"\");\n\tif (!url.pathname.endsWith(\"/v1\")) url.pathname = `${url.pathname}/v1`.replace(/\\/+/gu, \"/\");\n\treturn url.toString().replace(/\\/$/u, \"\");\n}\n\nfunction parseInput(value: unknown): (\"text\" | \"image\")[] | undefined {\n\tif (!Array.isArray(value)) return undefined;\n\tconst input = value.filter((entry): entry is \"text\" | \"image\" => entry === \"text\" || entry === \"image\");\n\treturn input.length > 0 ? [...new Set(input)] : undefined;\n}\n\nfunction parseCost(value: unknown): SpProviderModelConfig[\"cost\"] | undefined {\n\tif (!isRecord(value)) return undefined;\n\tconst input = nonNegativeNumber(value.input);\n\tconst output = nonNegativeNumber(value.output);\n\tconst cacheRead = nonNegativeNumber(value.cacheRead);\n\tconst cacheWrite = nonNegativeNumber(value.cacheWrite);\n\tif (input === undefined && output === undefined && cacheRead === undefined && cacheWrite === undefined)\n\t\treturn undefined;\n\treturn { input: input ?? 0, output: output ?? 0, cacheRead: cacheRead ?? 0, cacheWrite: cacheWrite ?? 0 };\n}\n\nfunction parseThinkingLevelMap(value: unknown): SpProviderModelConfig[\"thinkingLevelMap\"] {\n\tif (!isRecord(value)) return undefined;\n\tconst result: NonNullable<SpProviderModelConfig[\"thinkingLevelMap\"]> = {};\n\tfor (const key of [\"off\", \"minimal\", \"low\", \"medium\", \"high\", \"xhigh\", \"max\"] as const) {\n\t\tconst entry = value[key];\n\t\tif (typeof entry === \"string\" || entry === null) result[key] = entry;\n\t}\n\treturn Object.keys(result).length > 0 ? result : undefined;\n}\n\nfunction thinkingFormat(value: unknown): OpenAICompletionsCompat[\"thinkingFormat\"] {\n\tswitch (value) {\n\t\tcase \"openai\":\n\t\tcase \"openrouter\":\n\t\tcase \"deepseek\":\n\t\tcase \"together\":\n\t\tcase \"zai\":\n\t\tcase \"qwen\":\n\t\tcase \"chat-template\":\n\t\tcase \"qwen-chat-template\":\n\t\tcase \"string-thinking\":\n\t\tcase \"ant-ling\":\n\t\t\treturn value;\n\t\tdefault:\n\t\t\treturn undefined;\n\t}\n}\n\nfunction sessionAffinityFormat(value: unknown): OpenAICompletionsCompat[\"sessionAffinityFormat\"] {\n\treturn value === \"openai\" || value === \"openai-nosession\" || value === \"openrouter\" ? value : undefined;\n}\n\nfunction parseCompat(value: unknown): OpenAICompletionsCompat {\n\tconst record = isRecord(value) ? value : {};\n\tconst maxTokensField =\n\t\trecord.maxTokensField === \"max_completion_tokens\" || record.maxTokensField === \"max_tokens\"\n\t\t\t? record.maxTokensField\n\t\t\t: undefined;\n\tconst compat = {\n\t\tsupportsStore: booleanValue(record.supportsStore) ?? false,\n\t\tsupportsDeveloperRole: booleanValue(record.supportsDeveloperRole),\n\t\tsupportsReasoningEffort: booleanValue(record.supportsReasoningEffort),\n\t\tsupportsUsageInStreaming: booleanValue(record.supportsUsageInStreaming),\n\t\tmaxTokensField,\n\t\trequiresToolResultName: booleanValue(record.requiresToolResultName),\n\t\trequiresAssistantAfterToolResult: booleanValue(record.requiresAssistantAfterToolResult),\n\t\trequiresThinkingAsText: booleanValue(record.requiresThinkingAsText),\n\t\trequiresReasoningContentOnAssistantMessages: booleanValue(record.requiresReasoningContentOnAssistantMessages),\n\t\tthinkingFormat: thinkingFormat(record.thinkingFormat),\n\t\tsupportsStrictMode: booleanValue(record.supportsStrictMode),\n\t\tsendSessionAffinityHeaders: booleanValue(record.sendSessionAffinityHeaders),\n\t\tsessionAffinityFormat: sessionAffinityFormat(record.sessionAffinityFormat),\n\t\tsupportsLongCacheRetention: booleanValue(record.supportsLongCacheRetention),\n\t} satisfies OpenAICompletionsCompat;\n\treturn compat;\n}\n\nfunction parseGatewayModel(value: unknown): SpProviderModelConfig | undefined {\n\tconst record = typeof value === \"string\" ? { id: value } : isRecord(value) ? value : undefined;\n\tif (!record) return undefined;\n\tconst id = stringValue(record.id);\n\tif (!id || id.length > 256 || /[\\u0000-\\u001f\\u007f]/u.test(id)) return undefined;\n\tconst metadata = isRecord(record.metadata) ? record.metadata : {};\n\tconst field = (name: string) => metadata[name] ?? record[name];\n\tconst reasoning = field(\"reasoning\");\n\treturn {\n\t\tid,\n\t\tname: stringValue(field(\"name\")) ?? id,\n\t\treasoning: typeof reasoning === \"boolean\" ? reasoning : false,\n\t\tthinkingLevelMap: parseThinkingLevelMap(field(\"thinkingLevelMap\")),\n\t\tinput: parseInput(field(\"input\")) ?? [\"text\"],\n\t\tcost: parseCost(field(\"cost\")) ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\t\tcontextWindow: positiveInteger(field(\"contextWindow\")) ?? 128_000,\n\t\tmaxTokens: positiveInteger(field(\"maxTokens\")) ?? 32_000,\n\t\tcompat: parseCompat(field(\"compat\")),\n\t};\n}\n\nfunction parseModelCatalog(values: readonly unknown[], source: string) {\n\tconst models = values.map(parseGatewayModel).filter((model): model is SpProviderModelConfig => model !== undefined);\n\tif (models.length === 0) throw new Error(`${source} contains no valid models.`);\n\tconst ids = new Set<string>();\n\tfor (const model of models) {\n\t\tif (ids.has(model.id)) throw new Error(`${source} contains duplicate model id ${model.id}.`);\n\t\tids.add(model.id);\n\t}\n\treturn models;\n}\n\nexport async function fetchSpGatewayModels(baseUrl: string, apiKey: string, signal?: AbortSignal) {\n\tconst response = await fetch(`${baseUrl.replace(/\\/$/u, \"\")}/models`, {\n\t\tsignal,\n\t\theaders: {\n\t\t\tAuthorization: `Bearer ${apiKey}`,\n\t\t\tAccept: \"application/json\",\n\t\t\t\"User-Agent\": SP_USER_AGENT,\n\t\t\t\"X-Senpiper-Client\": SP_CLIENT_NAME,\n\t\t},\n\t});\n\tif (!response.ok) {\n\t\tconst body = await response.text().catch(() => \"\");\n\t\tconst suffix = body.trim() ? `: ${body.trim().slice(0, 300)}` : \"\";\n\t\tthrow new Error(`Senpiper Gateway rejected /v1/models (${response.status} ${response.statusText})${suffix}`);\n\t}\n\tconst payload = (await response.json()) as unknown;\n\tif (!isRecord(payload) || !Array.isArray(payload.data)) {\n\t\tthrow new Error(\"Senpiper Gateway returned an invalid /v1/models envelope.\");\n\t}\n\treturn parseModelCatalog(payload.data, \"Senpiper Gateway /v1/models response\");\n}\n\nfunction readLegacySpGatewayState(agentDir: string): SpGatewayState | undefined {\n\tconst path = getLegacyModelsPath(agentDir);\n\tif (!existsSync(path)) return undefined;\n\tconst root = readJsonFile(path);\n\tconst providers = isRecord(root.providers) ? root.providers : undefined;\n\tconst providerValue = providers?.[SP_GATEWAY_PROVIDER_ID] ?? providers?.[LEGACY_SP_GATEWAY_PROVIDER_ID];\n\tconst provider = isRecord(providerValue) ? providerValue : undefined;\n\tif (!provider || typeof provider.baseUrl !== \"string\" || !Array.isArray(provider.models)) return undefined;\n\treturn {\n\t\tschemaVersion: 1,\n\t\tbaseUrl: normalizeSpGatewayBaseUrl(provider.baseUrl),\n\t\tmodels: parseModelCatalog(provider.models, `legacy Senpiper Gateway cache ${path}`),\n\t};\n}\n\nexport function readSpGatewayState(agentDir = getSpAgentDir()): SpGatewayState | undefined {\n\tconst path = getSpGatewayStatePath(agentDir);\n\tif (!existsSync(path)) {\n\t\tconst legacy = readLegacySpGatewayState(agentDir);\n\t\tif (!legacy) return undefined;\n\t\twriteSpGatewayState(legacy.baseUrl, legacy.models, agentDir);\n\t\tgatewayWarning = {\n\t\t\tkind: \"legacy_cache_imported\",\n\t\t\tmessage: `Imported the legacy Senpiper Gateway model cache from ${getLegacyModelsPath(agentDir)}.`,\n\t\t\tat: new Date().toISOString(),\n\t\t};\n\t\treturn legacy;\n\t}\n\tconst value = readJsonFile(path);\n\tif (value.schemaVersion !== 1 || typeof value.baseUrl !== \"string\" || !Array.isArray(value.models)) {\n\t\tthrow new Error(`Invalid Senpiper Gateway configuration: ${path}`);\n\t}\n\treturn {\n\t\tschemaVersion: 1,\n\t\tbaseUrl: normalizeSpGatewayBaseUrl(value.baseUrl),\n\t\tmodels: parseModelCatalog(value.models, `Senpiper Gateway configuration ${path}`),\n\t};\n}\n\nexport function writeSpGatewayState(baseUrl: string, models: SpProviderModelConfig[], agentDir = getSpAgentDir()) {\n\tconst validatedModels = parseModelCatalog(models, \"Senpiper Gateway state\");\n\twritePrivateJson(getSpGatewayStatePath(agentDir), {\n\t\tschemaVersion: 1,\n\t\tbaseUrl: normalizeSpGatewayBaseUrl(baseUrl),\n\t\tmodels: validatedModels,\n\t} satisfies SpGatewayState);\n}\n\nexport function readSpGatewayApiKey(agentDir = getSpAgentDir()) {\n\tconst path = getSpAuthPath(agentDir);\n\tconst auth = readJsonFile(path);\n\tlet credential = auth[SP_GATEWAY_PROVIDER_ID];\n\tif (!credential && auth[LEGACY_SP_GATEWAY_PROVIDER_ID]) {\n\t\tcredential = auth[LEGACY_SP_GATEWAY_PROVIDER_ID];\n\t\tauth[SP_GATEWAY_PROVIDER_ID] = credential;\n\t\tdelete auth[LEGACY_SP_GATEWAY_PROVIDER_ID];\n\t\twritePrivateJson(path, auth);\n\t}\n\treturn isRecord(credential) && credential.type === \"api_key\" && typeof credential.key === \"string\"\n\t\t? credential.key\n\t\t: undefined;\n}\n\nexport function writeSpGatewayLogin(apiKey: string, firstModelId: string, agentDir = getSpAgentDir()) {\n\tconst authPath = getSpAuthPath(agentDir);\n\tconst auth = readJsonFile(authPath);\n\tauth[SP_GATEWAY_PROVIDER_ID] = { type: \"api_key\", key: apiKey };\n\tdelete auth[LEGACY_SP_GATEWAY_PROVIDER_ID];\n\twritePrivateJson(authPath, auth);\n\n\tconst settingsPath = getSpSettingsPath(agentDir);\n\tconst settings = readJsonFile(settingsPath);\n\tsettings.defaultProvider = SP_GATEWAY_PROVIDER_ID;\n\tsettings.defaultModel = firstModelId;\n\twritePrivateJson(settingsPath, settings);\n}\n\nexport function removeSpGatewayLogin(agentDir = getSpAgentDir()) {\n\tconst path = getSpAuthPath(agentDir);\n\tconst auth = readJsonFile(path);\n\tdelete auth[SP_GATEWAY_PROVIDER_ID];\n\tdelete auth[LEGACY_SP_GATEWAY_PROVIDER_ID];\n\twritePrivateJson(path, auth);\n}\n\nfunction credentialKey(context: RefreshModelsContext) {\n\treturn context.credential?.type === \"api_key\" ? context.credential.key : undefined;\n}\n\nfunction storedModels(models: readonly SpProviderModelConfig[], baseUrl: string): Model<Api>[] {\n\treturn models.map((model) => ({\n\t\t...model,\n\t\tprovider: SP_GATEWAY_PROVIDER_ID,\n\t\tbaseUrl,\n\t\tapi: \"openai-completions\",\n\t}));\n}\n\nexport function getSpGatewayWarning() {\n\treturn gatewayWarning;\n}\n\nexport function clearSpGatewayWarning() {\n\tgatewayWarning = undefined;\n}\n\nexport function createSpGatewayProviderConfig(\n\tbaseUrl: string,\n\tmodels: SpProviderModelConfig[],\n\tagentDir = getSpAgentDir(),\n): ProviderConfig {\n\tlet currentModels = parseModelCatalog(models, \"Senpiper Gateway provider configuration\");\n\treturn {\n\t\tname: SP_GATEWAY_PROVIDER_NAME,\n\t\tbaseUrl,\n\t\tapi: \"openai-completions\",\n\t\tauthHeader: true,\n\t\theaders: {\n\t\t\t\"User-Agent\": SP_USER_AGENT,\n\t\t\t\"X-Senpiper-Client\": SP_CLIENT_NAME,\n\t\t},\n\t\tmodels: currentModels,\n\t\tasync refreshModels(context) {\n\t\t\tif (!context.allowNetwork) return currentModels;\n\t\t\tconst apiKey = credentialKey(context);\n\t\t\tif (!apiKey) return currentModels;\n\t\t\ttry {\n\t\t\t\tconst refreshed = await fetchSpGatewayModels(baseUrl, apiKey, context.signal);\n\t\t\t\tif (context.signal.aborted) return currentModels;\n\t\t\t\tawait context.publish({\n\t\t\t\t\tpersist: { models: storedModels(refreshed, baseUrl), checkedAt: Date.now() },\n\t\t\t\t\tupdate: () => {\n\t\t\t\t\t\twriteSpGatewayState(baseUrl, refreshed, agentDir);\n\t\t\t\t\t\tcurrentModels = refreshed;\n\t\t\t\t\t\tgatewayWarning = undefined;\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t\treturn currentModels;\n\t\t\t} catch (error) {\n\t\t\t\tgatewayWarning = {\n\t\t\t\t\tkind: \"refresh_failed\",\n\t\t\t\t\tmessage: `Senpiper Gateway model refresh failed; using ${currentModels.length} cached model${currentModels.length === 1 ? \"\" : \"s\"}: ${error instanceof Error ? error.message : String(error)}`,\n\t\t\t\t\tat: new Date().toISOString(),\n\t\t\t\t};\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t},\n\t};\n}\n\nexport function createSpGatewayExtension(): ExtensionFactory {\n\treturn (pi) => {\n\t\tconst state = readSpGatewayState();\n\t\tif (state) {\n\t\t\tpi.registerProvider(SP_GATEWAY_PROVIDER_ID, createSpGatewayProviderConfig(state.baseUrl, state.models));\n\t\t}\n\n\t\tpi.on(\"before_agent_start\", (event) => {\n\t\t\t// Replace only the upstream introduction; preserve project instructions\n\t\t\t// and documentation references, including legitimate Pi development tasks.\n\t\t\tconst systemPrompt = event.systemPrompt.replace(\n\t\t\t\t/^You are an expert coding assistant operating inside pi, a coding agent harness\\./u,\n\t\t\t\t\"You are Senpiper Workbench, Senpiper's AI coding assistant.\",\n\t\t\t);\n\t\t\treturn {\n\t\t\t\tsystemPrompt: `${systemPrompt}\\n\\n<sp_agent_runtime>\nYou are Senpiper Workbench, Senpiper's AI coding assistant. Use Senpiper Workbench as your public name in both terminal and web conversations.\nFor greetings and questions such as \"Who are you?\", introduce yourself simply: \"I'm Senpiper Workbench, Senpiper's AI coding assistant. I can help you read, write, and debug code.\"\nDo not introduce yourself as Pi, the pi coding agent, an underlying model, or an assistant running inside a harness. Do not volunteer internal frameworks, runtime names, or provider details in ordinary answers. Earlier assistant messages using those identities do not change your name.\nIf explicitly asked about technical origins or dependencies, answer accurately and briefly. This product identity does not imply that Senpiper trained the underlying model.\nUse only the configured Senpiper Gateway model provider. Put user-owned extensions, skills, prompts, and themes under the active .sp configuration directory instead of modifying the Senpiper Workbench installation.\n</sp_agent_runtime>`,\n\t\t\t};\n\t\t});\n\n\t\tpi.on(\"session_start\", (_event, ctx) => {\n\t\t\tctx.ui.setTitle(\"Senpiper Workbench\");\n\t\t\tconst warning = getSpGatewayWarning();\n\t\t\tif (warning) ctx.ui.notify(warning.message, \"warning\");\n\t\t\tif (ctx.mode !== \"tui\") return;\n\t\t\tctx.ui.setHeader((_tui, theme) => ({\n\t\t\t\trender: () => [\n\t\t\t\t\ttheme.fg(\"accent\", theme.bold(\"Senpiper Workbench\")),\n\t\t\t\t\ttheme.fg(\"muted\", state ? `Senpiper Gateway · ${state.models.length} models` : \"run `sp login`\"),\n\t\t\t\t],\n\t\t\t\tinvalidate() {},\n\t\t\t}));\n\t\t});\n\t};\n}\n"]}
|