pi-system-one 1.2.4 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -23
- package/package.json +4 -1
- package/src/commands.ts +166 -78
- package/src/config.ts +64 -12
- package/src/extension.ts +23 -13
- package/src/persistence.ts +111 -0
- package/src/provider.ts +184 -0
- package/src/tool.ts +63 -7
- package/tests/commands.test.ts +252 -17
- package/tests/config.test.ts +59 -0
- package/tests/extension.test.ts +160 -1
- package/tests/persistence.test.ts +88 -0
- package/tests/provider.test.ts +329 -0
- package/tests/tool.test.ts +246 -0
package/README.md
CHANGED
|
@@ -6,6 +6,28 @@ System One decisions for Pi. Plug in TypeSafe Jev, Reflex, or your own provider.
|
|
|
6
6
|
pi install npm:pi-system-one
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
+
## Pi built-in classifier vs `pi-system-one` (as of Pi v1.0.3)
|
|
10
|
+
|
|
11
|
+
Pi already supports Jev through `codemode`. It can classify one question or combine classification with other tool calls in a script. This extension exposes decisions as a dedicated `system_one` tool, with a `/judge` prompt and a skill that guides when to use it. Choose based on your workflow, not on a claim that either route is more accurate.
|
|
12
|
+
|
|
13
|
+
| Need | Pi classifier through `codemode` | `pi-system-one` today |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| Setup | Enable `codemode`; sign in with `/login typesafe` or set `TYPESAFE_API_KEY`. | Install extension and sign in with `/login typesafe` or set `TYPESAFE_API_KEY`. Jev uses `jev-latest` by default. |
|
|
16
|
+
| Agent interface | Script calls `models.classify()`; you choose model, state, and questions. | Model calls `system_one` directly. `/judge` and bundled skill provide usage guidance. |
|
|
17
|
+
| One-off decisions | Write a `codemode` script even for a standalone classification. Useful if you already use scripts. | Call `system_one` without writing a script or enabling `codemode`. The same tool works mid-conversation when no other tool calls need orchestration. |
|
|
18
|
+
| Multiple tool calls | A script can gather data, call a classifier, and filter results before they reach the model. | The tool batches decision questions over supplied state, but does not replace `codemode` for scripting other tools. |
|
|
19
|
+
| Host availability | Requires a Pi host with `codemode` enabled and classifier models exposed to its scripts. | Loads as an extension without `codemode`; package also declares an OMP extension entry. Requires a host that supports this extension's APIs. |
|
|
20
|
+
| Endpoints | Pi's registered classifier models, including Jev across supported providers. | Any backend compatible with `POST /v1/systemone`, including local Reflex and custom endpoints. |
|
|
21
|
+
| Inputs | Pi classifier schema: object state, string question fields, `bool` for yes/no. | Accepts string or JSON state, `noul` for yes/no, and normalizes common argument aliases. |
|
|
22
|
+
| Score output | Score and confidence. Choice answers still include probabilities. | Score, confidence, per-level probabilities, and legend when backend supplies them. |
|
|
23
|
+
| Credentials | Pi resolves provider credentials, including saved `/login typesafe` key. | TypeSafe endpoint reuses Pi credentials per call. Explicit `SYSTEM_ONE_API_KEY` or session key takes precedence; custom endpoints never receive Pi's TypeSafe key. |
|
|
24
|
+
|
|
25
|
+
Pi's [v1.0.3 Codemode guide](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/codemode.md) documents scripts, model calls, and orchestration. Its [CLI guide](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/cli.md#enable-codemode) explains how to enable `codemode`. Pi extensions can also [call classifiers without Codemode](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/models.md#use-classifier-models); the table compares the agent-facing Codemode route with this extension's agent-facing tool, not every way to write a Pi extension.
|
|
26
|
+
|
|
27
|
+
Pi v1.0.3 [defines its Score answer](https://github.com/earendil-works/pi/blob/v1.0.3/packages/ai/src/types.ts#L660-L678) with only `score` and `confidence`. Its [response parser](https://github.com/earendil-works/pi/blob/v1.0.3/packages/ai/src/api/system-one-shared.ts#L80-L119) copies only those two Score fields. [TypeSafe's HTTP API](https://docs.typesafe.ai/api#score-answer) returns `probabilities` and `legend` too. Pi's [classifier input types](https://github.com/earendil-works/pi/blob/v1.0.3/packages/ai/src/types.ts#L635-L658) and [codemode docs](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/codemode.md#classify) document the narrower inputs shown above. These references describe Pi v1.0.3, not future releases.
|
|
28
|
+
|
|
29
|
+
If you already use `codemode` to combine tools and only need Pi's classifier response, use Pi's built-in route. For standalone decisions, choose this extension when you want a direct tool without a script or Codemode setup. It also provides prompt guidance, compatible custom endpoints, and the full Score distribution when the backend supplies it. The extension can run on a host without `codemode` if that host supports its extension APIs. Its OMP package entry makes it installable in OMP, but it does not guarantee support for every Pi or OMP version. `/so config` can save non-secret settings or select Pi's native classifier. Native mode rejects Score questions because Pi v1.0.3 does not expose their probability distribution. See [Pi classifier docs for v1.0.3](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/models.md#use-classifier-models).
|
|
30
|
+
|
|
9
31
|
For Oh My Pi (OMP), install the published package with:
|
|
10
32
|
|
|
11
33
|
```bash
|
|
@@ -20,7 +42,7 @@ omp install /path/to/pi-system-one
|
|
|
20
42
|
omp --extension /path/to/pi-system-one
|
|
21
43
|
```
|
|
22
44
|
|
|
23
|
-
|
|
45
|
+
The `system_one` tool registers at startup. Without settings, it uses TypeSafe Jev through this extension's HTTP provider and asks Pi for your TypeSafe credential at call time. Run `/login typesafe` first, or set `TYPESAFE_API_KEY`.
|
|
24
46
|
|
|
25
47
|
<!-- <img src="./demo.gif" alt="Description" width="640" > -->
|
|
26
48
|
|
|
@@ -29,41 +51,39 @@ Set `SYSTEM_ONE_BASE_URL` before launch, or run `/so config`; the `system_one` t
|
|
|
29
51
|
<img width="1836" height="1092" alt="demo" src="https://github.com/user-attachments/assets/341c0e4a-d0e5-4df0-85d2-74ca2f316db5" />
|
|
30
52
|
|
|
31
53
|
|
|
32
|
-
|
|
33
|
-
|
|
54
|
+
### Custom endpoint
|
|
55
|
+
|
|
56
|
+
For Reflex or another backend that supports `POST /v1/systemone`, run `/so config` and choose `custom`, or retain your existing environment setup:
|
|
57
|
+
|
|
34
58
|
```bash
|
|
35
59
|
export SYSTEM_ONE_BASE_URL=http://localhost:8008
|
|
60
|
+
export SYSTEM_ONE_MODEL=reflex
|
|
61
|
+
# Set SYSTEM_ONE_API_KEY only when your endpoint requires a key.
|
|
36
62
|
```
|
|
37
|
-
or
|
|
38
63
|
|
|
39
|
-
|
|
40
|
-
```bash
|
|
41
|
-
export SYSTEM_ONE_BASE_URL=https://api.typesafe.ai
|
|
42
|
-
export SYSTEM_ONE_MODEL=jev-latest
|
|
43
|
-
# Option 1: paste the key directly
|
|
44
|
-
export SYSTEM_ONE_API_KEY="..."
|
|
45
|
-
# Option 2: reuse an existing variable
|
|
46
|
-
export SYSTEM_ONE_API_KEY="$TYPESAFE_API_KEY"
|
|
47
|
-
```
|
|
64
|
+
Existing `SYSTEM_ONE_BASE_URL` takes precedence over saved settings. For TypeSafe, `jev-latest` is supplied automatically when no model is set. Other endpoints may require `SYSTEM_ONE_MODEL`.
|
|
48
65
|
|
|
49
66
|
|
|
50
67
|
---
|
|
51
68
|
|
|
52
|
-
|
|
69
|
+
## Configuration
|
|
53
70
|
|
|
54
|
-
|
|
71
|
+
Run `/so config` to choose `typesafe` (default HTTP path), `custom` (any System One endpoint), or `native` (Pi classifier). `/so config native` and `/so config typesafe` switch directly. Non-secret settings are saved to `<agent-dir>/pi-system-one.json`, where `<agent-dir>` is Pi's agent directory (`~/.pi/agent` by default or `PI_CODING_AGENT_DIR`). Terminal Pi and Paseo must use the same agent directory to share that file and Pi's saved login. Restart or reload an already-running client after the other client changes settings. Use `/so status` to inspect current mode and credential availability.
|
|
55
72
|
|
|
56
|
-
|
|
57
|
-
- `SYSTEM_ONE_MODEL` – optional default model name.
|
|
58
|
-
- `SYSTEM_ONE_API_KEY` – optional API key.
|
|
73
|
+
For example, a saved custom endpoint looks like this:
|
|
59
74
|
|
|
60
|
-
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"mode": "custom",
|
|
78
|
+
"baseUrl": "http://localhost:8008",
|
|
79
|
+
"model": "reflex",
|
|
80
|
+
"timeoutMs": 10000
|
|
81
|
+
}
|
|
82
|
+
```
|
|
61
83
|
|
|
62
|
-
|
|
63
|
-
- **Enter `-`** – forget the memory‑only key. If a `SYSTEM_ONE_API_KEY` environment variable is present, the client will fall back to it; otherwise the key becomes absent.
|
|
64
|
-
- **Leave the input empty** – keep the existing value.
|
|
84
|
+
Never store a key in JSON. A key entered into `/so config` stays in this session only. Leave key input blank to keep it; enter `-` to forget it. For custom endpoints, set `SYSTEM_ONE_BASE_URL` with `SYSTEM_ONE_API_KEY` if using environment credentials. An environment key without an environment endpoint never follows a saved custom URL. Custom URLs saved by `/so config` must be HTTP(S), without URL credentials, query, or fragment; put authentication in the key input or environment instead. Native mode supports Choice and Noul but rejects Score questions rather than hide missing probabilities. For full Score results, select `typesafe` or `custom`.
|
|
65
85
|
|
|
66
|
-
|
|
86
|
+
Environment endpoint and model values override saved settings at startup. A saved model is not reused when an environment endpoint points elsewhere. When `/so config` changes a custom URL, blank model input clears the previous endpoint's model; enter a model to keep using one. An explicit `/so config` choice takes effect immediately in this session. If both are set, the environment endpoint wins again on next startup.
|
|
67
87
|
|
|
68
88
|
---
|
|
69
89
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-system-one",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "System One decisions for Pi. Plug in TypeSafe Jev, Reflex, or your own provider.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,5 +42,8 @@
|
|
|
42
42
|
"extensions": [
|
|
43
43
|
"./src/extension.ts"
|
|
44
44
|
]
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@earendil-works/pi-coding-agent": "1.0.1"
|
|
45
48
|
}
|
|
46
49
|
}
|
package/src/commands.ts
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
|
-
// pi-system-one/src/commands.ts
|
|
2
1
|
import type {
|
|
3
2
|
ExtensionAPI,
|
|
4
3
|
ExtensionCommandContext,
|
|
5
4
|
} from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import { HttpSystemOneProvider } from "system-one-core";
|
|
7
5
|
import {
|
|
8
6
|
applyConfigAnswers,
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
type ConfigMode,
|
|
8
|
+
createSessionConfig,
|
|
11
9
|
describeConfig,
|
|
12
10
|
type SessionConfig,
|
|
11
|
+
TYPESAFE_BASE_URL,
|
|
12
|
+
TYPESAFE_MODEL,
|
|
13
13
|
} from "./config.ts";
|
|
14
|
+
import { saveStoredConfig } from "./persistence.ts";
|
|
15
|
+
import { isTypeSafeEndpoint, resolveSessionProvider } from "./provider.ts";
|
|
14
16
|
import { buildSystemOneTool } from "./tool.ts";
|
|
15
17
|
|
|
16
|
-
/** Mutable holder: empty until env or `/so config` provides a base URL. */
|
|
17
18
|
export interface SessionStore {
|
|
18
19
|
session?: SessionConfig;
|
|
19
20
|
}
|
|
@@ -21,106 +22,193 @@ export interface SessionStore {
|
|
|
21
22
|
export function registerSystemOneCommands(
|
|
22
23
|
pi: ExtensionAPI,
|
|
23
24
|
store: SessionStore,
|
|
25
|
+
configPath?: string,
|
|
24
26
|
) {
|
|
27
|
+
let registered = false;
|
|
25
28
|
function applyProvider() {
|
|
26
|
-
if (
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
});
|
|
35
|
-
pi.registerTool(buildSystemOneTool({ provider }));
|
|
29
|
+
if (registered) return;
|
|
30
|
+
registered = true;
|
|
31
|
+
pi.registerTool(
|
|
32
|
+
buildSystemOneTool({
|
|
33
|
+
resolveProvider: (ctx) =>
|
|
34
|
+
resolveSessionProvider(store.session ?? createSessionConfig(), ctx),
|
|
35
|
+
}),
|
|
36
|
+
);
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
async function cmdConfig(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
39
|
+
async function cmdConfig(
|
|
40
|
+
ctx: ExtensionCommandContext,
|
|
41
|
+
requestedMode?: string,
|
|
42
|
+
): Promise<void> {
|
|
43
|
+
const current = store.session ?? createSessionConfig();
|
|
44
|
+
const modes: ConfigMode[] = [
|
|
45
|
+
current.mode,
|
|
46
|
+
...(["typesafe", "custom", "native"] as ConfigMode[]).filter(
|
|
47
|
+
(mode) => mode !== current.mode,
|
|
48
|
+
),
|
|
49
|
+
];
|
|
50
|
+
const descriptions: Record<ConfigMode, string> = {
|
|
51
|
+
typesafe: "TypeSafe Jev (HTTP, full Score)",
|
|
52
|
+
custom: "Custom System One endpoint",
|
|
53
|
+
native: "Pi classifier (Choice and Noul only)",
|
|
54
|
+
};
|
|
55
|
+
const options = modes.map(
|
|
56
|
+
(mode) =>
|
|
57
|
+
`${mode} — ${descriptions[mode]}${mode === current.mode ? " (current)" : ""}`,
|
|
56
58
|
);
|
|
57
|
-
|
|
59
|
+
const modeRaw =
|
|
60
|
+
requestedMode ??
|
|
61
|
+
(await ctx.ui.select("System One provider mode", options));
|
|
62
|
+
if (modeRaw === undefined) {
|
|
58
63
|
ctx.ui.notify("Cancelled.", "info");
|
|
59
64
|
return;
|
|
60
65
|
}
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (
|
|
66
|
-
ctx.ui.notify("
|
|
66
|
+
const mode =
|
|
67
|
+
requestedMode === undefined
|
|
68
|
+
? modes[options.indexOf(modeRaw)]
|
|
69
|
+
: (modeRaw.trim().toLowerCase() as ConfigMode);
|
|
70
|
+
if (!mode || !modes.includes(mode)) {
|
|
71
|
+
ctx.ui.notify("Choose typesafe, custom, or native.", "error");
|
|
67
72
|
return;
|
|
68
73
|
}
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
timeoutMs: "",
|
|
74
|
+
const next: SessionConfig = {
|
|
75
|
+
current: { ...current.current },
|
|
76
|
+
mode,
|
|
77
|
+
keyInMemory: current.keyInMemory,
|
|
74
78
|
};
|
|
75
|
-
if (
|
|
76
|
-
|
|
79
|
+
if (mode === "typesafe") {
|
|
80
|
+
next.current.baseUrl = TYPESAFE_BASE_URL;
|
|
81
|
+
next.current.model = TYPESAFE_MODEL;
|
|
82
|
+
// A key exported for a custom endpoint must not follow a mode switch.
|
|
83
|
+
next.current.apiKey =
|
|
84
|
+
!process.env.SYSTEM_ONE_BASE_URL ||
|
|
85
|
+
isTypeSafeEndpoint(process.env.SYSTEM_ONE_BASE_URL)
|
|
86
|
+
? process.env.SYSTEM_ONE_API_KEY || undefined
|
|
87
|
+
: undefined;
|
|
88
|
+
next.keyInMemory = false;
|
|
89
|
+
} else if (mode === "native") {
|
|
90
|
+
next.current.baseUrl = "";
|
|
91
|
+
next.current.model = TYPESAFE_MODEL;
|
|
92
|
+
next.current.apiKey = undefined;
|
|
93
|
+
next.keyInMemory = false;
|
|
94
|
+
} else {
|
|
95
|
+
const baseUrlRaw = await ctx.ui.input(
|
|
96
|
+
`SYSTEM_ONE_BASE_URL (current: ${current.mode === "custom" ? current.current.baseUrl : "unset"}):`,
|
|
97
|
+
current.mode === "custom" ? current.current.baseUrl : "",
|
|
98
|
+
);
|
|
99
|
+
if (baseUrlRaw === undefined) {
|
|
100
|
+
ctx.ui.notify("Cancelled.", "info");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const baseUrl =
|
|
104
|
+
baseUrlRaw.trim() ||
|
|
105
|
+
(current.mode === "custom" ? current.current.baseUrl : "");
|
|
106
|
+
if (!baseUrl) {
|
|
107
|
+
ctx.ui.notify("Cancelled (base URL is required).", "info");
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const sameEndpoint =
|
|
111
|
+
current.mode === "custom" && baseUrl === current.current.baseUrl;
|
|
112
|
+
const modelRaw = await ctx.ui.input(
|
|
113
|
+
`SYSTEM_ONE_MODEL (current: ${sameEndpoint ? (current.current.model ?? "server default") : "server default"}):`,
|
|
114
|
+
sameEndpoint ? (current.current.model ?? "") : "",
|
|
115
|
+
);
|
|
116
|
+
if (modelRaw === undefined) {
|
|
117
|
+
ctx.ui.notify("Cancelled.", "info");
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const apiKeyRaw = await ctx.ui.input(
|
|
121
|
+
"SYSTEM_ONE_API_KEY (empty = keep, - = forget session key; never saved to JSON):",
|
|
122
|
+
"",
|
|
123
|
+
);
|
|
124
|
+
if (apiKeyRaw === undefined) {
|
|
125
|
+
ctx.ui.notify("Cancelled.", "info");
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
next.current.baseUrl = baseUrl;
|
|
129
|
+
next.current.model =
|
|
130
|
+
modelRaw.trim() || (sameEndpoint ? current.current.model : undefined);
|
|
131
|
+
if (!sameEndpoint) {
|
|
132
|
+
// Never carry a key entered for one endpoint to another endpoint.
|
|
133
|
+
next.current.apiKey = undefined;
|
|
134
|
+
next.keyInMemory = false;
|
|
135
|
+
}
|
|
136
|
+
if (apiKeyRaw.trim() === "-") {
|
|
137
|
+
next.current.apiKey =
|
|
138
|
+
sameEndpoint && process.env.SYSTEM_ONE_BASE_URL === baseUrl
|
|
139
|
+
? process.env.SYSTEM_ONE_API_KEY || undefined
|
|
140
|
+
: undefined;
|
|
141
|
+
next.keyInMemory = false;
|
|
142
|
+
} else if (apiKeyRaw.trim()) {
|
|
143
|
+
applyConfigAnswers(next, {
|
|
144
|
+
baseUrl: "",
|
|
145
|
+
model: "",
|
|
146
|
+
apiKey: apiKeyRaw.trim(),
|
|
147
|
+
timeoutMs: "",
|
|
148
|
+
});
|
|
149
|
+
}
|
|
77
150
|
}
|
|
78
|
-
|
|
79
|
-
|
|
151
|
+
if (configPath) {
|
|
152
|
+
try {
|
|
153
|
+
saveStoredConfig(configPath, {
|
|
154
|
+
mode,
|
|
155
|
+
...(mode === "custom"
|
|
156
|
+
? {
|
|
157
|
+
baseUrl: next.current.baseUrl,
|
|
158
|
+
...(next.current.model ? { model: next.current.model } : {}),
|
|
159
|
+
}
|
|
160
|
+
: {}),
|
|
161
|
+
timeoutMs: next.current.timeoutMs,
|
|
162
|
+
});
|
|
163
|
+
} catch (error) {
|
|
164
|
+
ctx.ui.notify(
|
|
165
|
+
`Could not save settings: ${error instanceof Error ? error.message : String(error)}`,
|
|
166
|
+
"error",
|
|
167
|
+
);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
store.session = next;
|
|
80
172
|
ctx.ui.notify(
|
|
81
|
-
|
|
82
|
-
? "Updated
|
|
83
|
-
:
|
|
173
|
+
next.keyInMemory
|
|
174
|
+
? "Updated. API key stays in memory only; other settings were saved."
|
|
175
|
+
: configPath
|
|
176
|
+
? "Settings saved."
|
|
177
|
+
: "Updated for this session.",
|
|
84
178
|
"info",
|
|
85
179
|
);
|
|
86
180
|
}
|
|
87
181
|
|
|
88
182
|
async function cmdStatus(ctx: ExtensionCommandContext): Promise<void> {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
183
|
+
const session = store.session ?? createSessionConfig();
|
|
184
|
+
let summary = describeConfig(session);
|
|
185
|
+
if (
|
|
186
|
+
session.mode !== "native" &&
|
|
187
|
+
!session.current.apiKey &&
|
|
188
|
+
isTypeSafeEndpoint(session.current.baseUrl)
|
|
189
|
+
) {
|
|
190
|
+
const key = await ctx.modelRegistry?.getApiKeyForProvider?.("typesafe");
|
|
191
|
+
summary = summary.replace(
|
|
192
|
+
"api key: absent",
|
|
193
|
+
key
|
|
194
|
+
? "api key: Pi TypeSafe credentials available"
|
|
195
|
+
: "api key: absent (run /login typesafe)",
|
|
93
196
|
);
|
|
94
|
-
return;
|
|
95
197
|
}
|
|
96
|
-
ctx.ui.notify(
|
|
198
|
+
ctx.ui.notify(summary, "info");
|
|
97
199
|
}
|
|
98
200
|
|
|
99
|
-
const completions = [
|
|
100
|
-
{
|
|
101
|
-
value: "config",
|
|
102
|
-
label: "config - change endpoint, model, or key",
|
|
103
|
-
description: "Reconfigure System One for this session",
|
|
104
|
-
},
|
|
105
|
-
{
|
|
106
|
-
value: "status",
|
|
107
|
-
label: "status - show current config",
|
|
108
|
-
description: "Show current endpoint, model, and key source",
|
|
109
|
-
},
|
|
110
|
-
];
|
|
111
|
-
|
|
112
201
|
pi.registerCommand("so", {
|
|
113
202
|
description: "System One config (/so status for current)",
|
|
114
|
-
getArgumentCompletions: (prefix: string) =>
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
203
|
+
getArgumentCompletions: (prefix: string) =>
|
|
204
|
+
["config", "status"]
|
|
205
|
+
.filter((c) => c.startsWith(prefix.trim().toLowerCase()))
|
|
206
|
+
.map((c) => ({ value: c, label: c })),
|
|
118
207
|
handler: async (args, ctx) => {
|
|
119
|
-
const [cmd] = args.trim().toLowerCase().split(/\s+/);
|
|
208
|
+
const [cmd, mode] = args.trim().toLowerCase().split(/\s+/);
|
|
120
209
|
if (cmd === "status") await cmdStatus(ctx);
|
|
121
|
-
else await cmdConfig(ctx);
|
|
210
|
+
else await cmdConfig(ctx, cmd === "config" ? mode : undefined);
|
|
122
211
|
},
|
|
123
212
|
});
|
|
124
|
-
|
|
125
213
|
return { applyProvider, store };
|
|
126
214
|
}
|
package/src/config.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
// pi-system-one/src/config.ts
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// (e.g. SYSTEM_ONE_API_KEY) in the shell.
|
|
3
|
+
// Non-secret settings can persist in the user agent directory. Environment
|
|
4
|
+
// variables retain precedence at startup. API keys are never written by this
|
|
5
|
+
// extension: /so config keys live only in memory; Pi login or explicit
|
|
6
|
+
// environment credentials provide persistent authentication.
|
|
8
7
|
|
|
9
8
|
export interface SystemOneEnv {
|
|
10
9
|
SYSTEM_ONE_BASE_URL?: string;
|
|
@@ -21,6 +20,17 @@ export interface SystemOneExtConfig {
|
|
|
21
20
|
}
|
|
22
21
|
|
|
23
22
|
export const DEFAULT_TIMEOUT_MS = 10_000;
|
|
23
|
+
export const TYPESAFE_BASE_URL = "https://api.typesafe.ai";
|
|
24
|
+
export const TYPESAFE_MODEL = "jev-latest";
|
|
25
|
+
export type ConfigMode = "typesafe" | "custom" | "native";
|
|
26
|
+
|
|
27
|
+
/** Public settings only. Credentials never belong in this file. */
|
|
28
|
+
export interface StoredConfig {
|
|
29
|
+
mode?: ConfigMode;
|
|
30
|
+
baseUrl?: string;
|
|
31
|
+
model?: string;
|
|
32
|
+
timeoutMs?: number;
|
|
33
|
+
}
|
|
24
34
|
|
|
25
35
|
export function loadSystemOneConfig(
|
|
26
36
|
env: SystemOneEnv = process.env,
|
|
@@ -48,12 +58,49 @@ export interface SessionConfig {
|
|
|
48
58
|
current: SystemOneExtConfig;
|
|
49
59
|
/** True when the key came from `/so config` (memory-only). */
|
|
50
60
|
keyInMemory: boolean;
|
|
61
|
+
mode: ConfigMode;
|
|
51
62
|
}
|
|
52
63
|
|
|
53
64
|
export function createSessionConfig(
|
|
54
65
|
env: SystemOneEnv = process.env,
|
|
66
|
+
stored: StoredConfig = {},
|
|
55
67
|
): SessionConfig {
|
|
56
|
-
|
|
68
|
+
const mode: ConfigMode = env.SYSTEM_ONE_BASE_URL
|
|
69
|
+
? "custom"
|
|
70
|
+
: (stored.mode ?? "typesafe");
|
|
71
|
+
const baseUrl =
|
|
72
|
+
mode === "native"
|
|
73
|
+
? ""
|
|
74
|
+
: (env.SYSTEM_ONE_BASE_URL ??
|
|
75
|
+
(mode === "typesafe" ? TYPESAFE_BASE_URL : (stored.baseUrl ?? "")));
|
|
76
|
+
const rawTimeout = env.SYSTEM_ONE_TIMEOUT_MS
|
|
77
|
+
? Number(env.SYSTEM_ONE_TIMEOUT_MS)
|
|
78
|
+
: (stored.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
79
|
+
if (!Number.isFinite(rawTimeout) || rawTimeout <= 0)
|
|
80
|
+
throw new Error("SYSTEM_ONE_TIMEOUT_MS must be a positive number");
|
|
81
|
+
return {
|
|
82
|
+
mode,
|
|
83
|
+
current: {
|
|
84
|
+
baseUrl,
|
|
85
|
+
// An env key without an env endpoint must not follow a saved custom URL.
|
|
86
|
+
apiKey:
|
|
87
|
+
mode === "native" ||
|
|
88
|
+
(stored.mode === "custom" && !env.SYSTEM_ONE_BASE_URL)
|
|
89
|
+
? undefined
|
|
90
|
+
: env.SYSTEM_ONE_API_KEY || undefined,
|
|
91
|
+
model:
|
|
92
|
+
mode === "native"
|
|
93
|
+
? TYPESAFE_MODEL
|
|
94
|
+
: env.SYSTEM_ONE_MODEL ||
|
|
95
|
+
(env.SYSTEM_ONE_BASE_URL &&
|
|
96
|
+
env.SYSTEM_ONE_BASE_URL !== stored.baseUrl
|
|
97
|
+
? undefined
|
|
98
|
+
: stored.model) ||
|
|
99
|
+
(mode === "typesafe" ? TYPESAFE_MODEL : undefined),
|
|
100
|
+
timeoutMs: rawTimeout,
|
|
101
|
+
},
|
|
102
|
+
keyInMemory: false,
|
|
103
|
+
};
|
|
57
104
|
}
|
|
58
105
|
|
|
59
106
|
/** Empty session for `/so config` when no env is present. */
|
|
@@ -66,6 +113,7 @@ export function blankSession(): SessionConfig {
|
|
|
66
113
|
timeoutMs: DEFAULT_TIMEOUT_MS,
|
|
67
114
|
},
|
|
68
115
|
keyInMemory: false,
|
|
116
|
+
mode: "custom",
|
|
69
117
|
};
|
|
70
118
|
}
|
|
71
119
|
|
|
@@ -106,14 +154,18 @@ export function applyConfigAnswers(
|
|
|
106
154
|
/** Human-readable summary. Shows whether a key exists, never the key. */
|
|
107
155
|
export function describeConfig(session: SessionConfig): string {
|
|
108
156
|
const c = session.current;
|
|
109
|
-
const keyLine =
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
157
|
+
const keyLine =
|
|
158
|
+
session.mode === "native"
|
|
159
|
+
? " api key: managed by Pi (native classifier)"
|
|
160
|
+
: !c.apiKey
|
|
161
|
+
? " api key: absent"
|
|
162
|
+
: session.keyInMemory
|
|
163
|
+
? " api key: in memory only (gone when the session closes; export SYSTEM_ONE_API_KEY to persist)"
|
|
164
|
+
: " api key: from environment";
|
|
114
165
|
return [
|
|
115
166
|
"System One:",
|
|
116
|
-
`
|
|
167
|
+
` mode: ${session.mode}`,
|
|
168
|
+
` endpoint: ${c.baseUrl || "none"}`,
|
|
117
169
|
keyLine,
|
|
118
170
|
` model: ${c.model ?? "server default"}`,
|
|
119
171
|
` timeout: ${c.timeoutMs}ms`,
|
package/src/extension.ts
CHANGED
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import
|
|
5
|
+
import {
|
|
6
|
+
type ExtensionAPI,
|
|
7
|
+
getAgentDir,
|
|
8
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
9
|
import { registerSystemOneCommands, type SessionStore } from "./commands.ts";
|
|
7
10
|
import { createSessionConfig } from "./config.ts";
|
|
11
|
+
import { loadStoredConfig } from "./persistence.ts";
|
|
8
12
|
|
|
9
13
|
/**
|
|
10
14
|
* Directory holding the bundled `system-one` skill (use-case catalog).
|
|
@@ -22,23 +26,29 @@ export function systemOnePromptsDir(): string {
|
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
export default function piSystemOneExtension(pi: ExtensionAPI) {
|
|
25
|
-
|
|
26
|
-
// no env set. The tool appears once a base URL exists (env or /so config).
|
|
29
|
+
const configPath = join(getAgentDir(), "pi-system-one.json");
|
|
27
30
|
const store: SessionStore = {};
|
|
28
31
|
try {
|
|
29
|
-
store.session = createSessionConfig(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
32
|
+
store.session = createSessionConfig(
|
|
33
|
+
process.env,
|
|
34
|
+
loadStoredConfig(configPath),
|
|
35
|
+
);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
38
|
+
try {
|
|
39
|
+
store.session = createSessionConfig(process.env);
|
|
40
|
+
} catch {
|
|
41
|
+
store.session = createSessionConfig({});
|
|
42
|
+
}
|
|
43
|
+
pi.on("session_start", (_event, ctx) => {
|
|
44
|
+
ctx.ui.notify(
|
|
45
|
+
`System One: ${message}. Ignoring saved settings; fix them or run /so config.`,
|
|
46
|
+
"warning",
|
|
47
|
+
);
|
|
38
48
|
});
|
|
39
49
|
}
|
|
40
50
|
|
|
41
|
-
const { applyProvider } = registerSystemOneCommands(pi, store);
|
|
51
|
+
const { applyProvider } = registerSystemOneCommands(pi, store, configPath);
|
|
42
52
|
applyProvider();
|
|
43
53
|
|
|
44
54
|
// Contribute the bundled `system-one` skill (use-case catalog) and the
|