pi-daybreak-param 0.1.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/LICENSE +21 -0
- package/README.md +77 -0
- package/docs/research.md +54 -0
- package/package.json +17 -0
- package/src/config.ts +52 -0
- package/src/controller.ts +104 -0
- package/src/index.ts +9 -0
- package/src/payload.ts +47 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MercuriusDream
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# pi-daybreak-param
|
|
2
|
+
|
|
3
|
+
Persistent `/daybreak` configuration and explicit `access_programs.cyber` request selection for Pi's **new OpenAI → Sign in with ChatGPT** integration. Tested against Pi **0.99.1**.
|
|
4
|
+
|
|
5
|
+
**Experimental for ChatGPT direct-token authentication:** OpenAI documents the public Responses endpoint and the Daybreak request field separately, but does not explicitly confirm Daybreak support/entitlement for Sign in with ChatGPT direct-token grants. This extension sends the field; it does not grant access or prove that the server applied it. See [research](docs/research.md).
|
|
6
|
+
|
|
7
|
+
## Try it
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pi -e ./src/index.ts --daybreak-cyber daybreak_blue
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Install from GitHub:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
pi install git:github.com/MercuriusDream/pi-daybreak-param
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Once published to npm:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
pi install npm:pi-daybreak-param
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
For local development: `pi install /absolute/path/to/pi-daybreak-param`. Reload an active Pi session with `/reload` after installation.
|
|
26
|
+
|
|
27
|
+
Use your existing **OpenAI / Sign in with ChatGPT** login. Authentication and token refresh remain Pi's responsibility. The wrapper also applies to API-key authentication on the same OpenAI provider. It does not modify the legacy `openai-codex` provider.
|
|
28
|
+
|
|
29
|
+
### Commands
|
|
30
|
+
|
|
31
|
+
```text
|
|
32
|
+
/daybreak Open the configuration picker
|
|
33
|
+
/daybreak status Show requested selection or configuration error
|
|
34
|
+
/daybreak daybreak_blue Explicitly request Blue
|
|
35
|
+
/daybreak daybreak_red Explicitly request Red
|
|
36
|
+
/daybreak standard Explicit opt-out: request standard safeguards
|
|
37
|
+
/daybreak default Remove this extension's override (inherit existing/server behavior)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`standard` and `default` are **not the same**. Inheriting defaults may still enable Daybreak for an eligible account, and does not remove a field configured elsewhere. Use `standard` for explicit opt-out. The model must support the chosen program; errors are not retried with another tier or model.
|
|
41
|
+
|
|
42
|
+
Menu and command selections are saved atomically to `~/.pi/agent/daybreak.json` (or the directory specified by `PI_CODING_AGENT_DIR`). They apply immediately and survive reloads/new sessions. The CLI flag is a startup override and is not saved; at the next session start it takes precedence over the saved choice. A command changes the current choice and saves it, but does not modify the CLI flag.
|
|
43
|
+
|
|
44
|
+
An absent config intentionally means inherit/default. A malformed or unreadable config is an error that blocks OpenAI requests—even with a valid CLI flag. Correct it explicitly using `/daybreak <selection>` or repair the file. Other providers are outside the wrapper's scope.
|
|
45
|
+
|
|
46
|
+
## Why `/daybreak`, not `/settings`?
|
|
47
|
+
|
|
48
|
+
Pi 0.99.1's built-in `/settings` has hardcoded rows and callbacks, with no supported extension setting registration API. `/daybreak` therefore provides its own supported selection dialog and persistent configuration, without monkey-patching Pi or replacing the built-in settings screen.
|
|
49
|
+
|
|
50
|
+
## Errors, not silent fallback
|
|
51
|
+
|
|
52
|
+
The extension uses a thin provider-stream wrapper rather than the extension payload event: Pi catches payload-event handler exceptions and continues, which cannot enforce fail-closed validation.
|
|
53
|
+
|
|
54
|
+
- Invalid CLI selection blocks OpenAI calls and becomes a provider error, including in non-UI modes. Correct it explicitly via `/daybreak` or restart with a valid flag.
|
|
55
|
+
- Invalid command input is an error, leaving the prior selection unchanged.
|
|
56
|
+
- Invalid/unreadable stored configuration blocks requests. Save failures also block requests rather than silently continuing with stale settings.
|
|
57
|
+
- With an explicit selection, malformed payloads/access-program objects, mismatched request model IDs, and noncanonical endpoints fail **before HTTP**.
|
|
58
|
+
- API permission, unsupported-program, and model-compatibility errors are preserved. No downgrade, field omission retry, or alternate-model retry is added.
|
|
59
|
+
- The status displays **requested, not verified** access. No credential or payload logging.
|
|
60
|
+
|
|
61
|
+
The provider registration preserves built-in models/authentication and delegates to Pi's own OpenAI Responses implementation, including tools, cancellation, streaming, usage, and instrumentation. It uses the actual request model, not the selected-model approximation from `before_provider_request`.
|
|
62
|
+
|
|
63
|
+
## Tests
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
bun test
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Optional offline wire tests against an installed Pi AI distribution:
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
PI_AI_TEST_DIST=/path/to/@earendil-works/pi-ai/dist \
|
|
73
|
+
PI_CODING_AGENT_TEST_DIST=/path/to/@earendil-works/pi-coding-agent/dist \
|
|
74
|
+
bun test
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
All wire-test HTTP is intercepted with a dummy token. Tests cover serialization and denied requests with no downgrade, plus validation failures that make **zero HTTP calls**. The optional provider-composition test also verifies that Pi's real composer retains the built-in OpenAI models, API-key authentication, and ChatGPT subscription authentication. Tests do not test real account approval or live Daybreak behavior.
|
package/docs/research.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Research and integration notes
|
|
2
|
+
|
|
3
|
+
## Pi version inspected
|
|
4
|
+
|
|
5
|
+
Installed `@earendil-works/pi-coding-agent` **0.99.1**, including the installed JavaScript, declarations, and bundled source-map TypeScript. This is a version-specific source review, not a claim about an uninspected upstream revision.
|
|
6
|
+
|
|
7
|
+
Relevant paths under Pi's `packages/` source layout:
|
|
8
|
+
|
|
9
|
+
- `ai/src/providers/openai.ts`: the new `openai` provider offers API-key auth and **Sign in with ChatGPT**, using the public Responses API.
|
|
10
|
+
- `ai/src/auth/oauth/openai-chatgpt.ts`: dynamic public OAuth client, `resource=https://api.openai.com/v1`, scope `chatgpt.tokens.use.direct`. Its `toAuth` returns the access token to the existing API implementation. The extension does not change this flow.
|
|
11
|
+
- `ai/src/providers/openai-codex.ts`: a separate, explicitly legacy provider using `https://chatgpt.com/backend-api` and `openai-codex-responses`. Not the target of this extension.
|
|
12
|
+
- `ai/src/api/openai-responses.ts`: `buildParams` has no named `access_programs` field, but merges generic `samplingParams` last. `stream` invokes `options.onPayload` before calling `client.responses.create`. Its ChatGPT-specific field omissions do not strip an extension-added `access_programs` value.
|
|
13
|
+
- `coding-agent/src/core/sdk.ts`: the payload callback dispatches extension `before_provider_request` handlers.
|
|
14
|
+
- `coding-agent/src/core/extensions/runner.ts`: each handler receives the prior handler's payload; a non-undefined return replaces the whole payload. Handler exceptions are reported, not an effective way to block the request.
|
|
15
|
+
- `coding-agent/src/core/extensions/types.ts`: payload event contains only `payload`, not request model/auth metadata. The context exposes the selected model, which is not necessarily the routed physical request model.
|
|
16
|
+
- `coding-agent/examples/extensions/provider-payload.ts`: reference for returning the replacement payload directly.
|
|
17
|
+
|
|
18
|
+
There is **no explicit built-in Daybreak switch or `access_programs.cyber` option** in the inspected Pi request implementation. However, generic `samplingParams` can already carry the field. Therefore an extension is a convenient command/flag interface, not the only way to serialize the parameter.
|
|
19
|
+
|
|
20
|
+
## OpenAI documentation
|
|
21
|
+
|
|
22
|
+
- [Responses API Daybreak guide](https://developers.openai.com/api/docs/guides/daybreak): `access_programs.cyber` accepts `standard`, `daybreak_blue`, `daybreak_red`; optional omission lets the server choose its defaults. The parameter selects approved access, it does not grant approval. Model compatibility matters. `null` is not a request value.
|
|
23
|
+
- [Daybreak overview](https://help.openai.com/en/articles/20001258-openai-daybreak-trusted-access-for-cyber-overview) and [migration guide](https://help.openai.com/en/articles/20001532-openai-daybreak-migration-org-consolidation-guide): official Codex signed in with ChatGPT has a Daybreak toggle in its Advanced model picker, with account/workspace eligibility controls. This does not document the legacy backend wire protocol or prove third-party direct-token entitlement.
|
|
24
|
+
|
|
25
|
+
The new Pi sign-in flow reaches the documented public endpoint, so the extension can serialize the documented field. **Whether the SIWC route accepts/honors that field, and whether a particular direct ChatGPT OAuth grant is entitled to Daybreak, are not established by source inspection or offline tests.** The API-key/project examples in the guide should not be presented as proof of subscription-token approval. No real credentials were read and no authenticated requests were made during development.
|
|
26
|
+
|
|
27
|
+
Research used indexed official-page text; direct fetch of the Daybreak guide failed through the available fetch service. Treat rollout and entitlement details as something to confirm with OpenAI for your account.
|
|
28
|
+
|
|
29
|
+
## Extension contracts and boundaries
|
|
30
|
+
|
|
31
|
+
Uses `pi.on`, `registerFlag`, `registerCommand`, `ctx.ui.setStatus`, and a thin `registerProvider("openai", { api, streamSimple })` wrapper around Pi's own OpenAI Responses implementation. The initially considered payload-event approach was removed because its swallowed exceptions cannot satisfy the user's fail-closed requirement.
|
|
32
|
+
|
|
33
|
+
- `coding-agent/src/core/provider-composer.ts` confirms that a stream-only legacy registration preserves the existing models and composes both built-in authentication methods when neither models nor auth overrides are supplied.
|
|
34
|
+
- The wrapper composes the existing `onPayload` callback, then validates/transforms the body using the **actual request model**. Callback errors propagate through the OpenAI implementation into a terminal provider error before HTTP.
|
|
35
|
+
- Loading the factory only registers capabilities; no background resources, timers, credential access, or startup I/O.
|
|
36
|
+
- Explicit `default` is no override; `standard` is explicit opt-out. Explicit selection preserves other top-level request fields and other access-program keys.
|
|
37
|
+
- Invalid CLI configuration is retained as an error state that blocks OpenAI calls; it is not converted into default behavior. Invalid commands throw. Invalid payloads/endpoint/model identity throw when applying an explicit selection.
|
|
38
|
+
- Selection changes wait for idle. Menu/command selections persist atomically in the agent directory's `daybreak.json`. Each `session_start` validates that file, then selects the CLI override or saved value. Invalid reads/writes block OpenAI requests; only a genuinely absent config has the intentional initial inherit state.
|
|
39
|
+
- UI status is guarded by `ctx.hasUI`. CLI flag works in print/JSON/RPC modes; no custom terminal UI is needed.
|
|
40
|
+
- Scope: requests through the `openai` provider and `openai-responses` API. An explicit program requires the canonical `https://api.openai.com/v1` endpoint and matching payload model ID. Both auth methods are preserved. Other providers, including legacy Codex, are outside this registration.
|
|
41
|
+
- Existing payload handlers run before this final transformation. `default` does not remove values configured in `samplingParams` or added elsewhere. Another provider replacement can conflict with this registration.
|
|
42
|
+
- No automatic retries with another program/model, permission-error masking, approval claims, or removal of safeguards.
|
|
43
|
+
|
|
44
|
+
### SIWC-specific documentation
|
|
45
|
+
|
|
46
|
+
[Models and inference](https://developers.openai.com/siwc/token-sharing-open-source/models-and-inference) documents direct OAuth-token use at `api.openai.com/v1/responses` and instructs callers not to use the ChatGPT backend endpoints. [Token reference](https://developers.openai.com/siwc/token-sharing-open-source/token-reference) documents the audience and direct-token scope. [Preview limitations](https://developers.openai.com/siwc/token-sharing-open-source/preview-limitations) does not list `access_programs` among unsupported fields, but that omission is **not an explicit support guarantee**. Daybreak approval on another product surface is not documented as transferring to SIWC.
|
|
47
|
+
|
|
48
|
+
### Built-in settings limitation
|
|
49
|
+
|
|
50
|
+
A dedicated source review confirmed no supported extension API/event to append settings rows in Pi 0.99.1. `interactive-mode.ts` intercepts `/settings`, constructs the selector with explicit built-in configuration/callbacks, and `components/settings-selector.ts` builds hardcoded items and dispatches a hardcoded switch. Extension context settings are not a mutation/registration interface. The user subsequently chose `/daybreak` instead of a core change. The extension now provides a supported `/daybreak` selection dialog with persistent configuration; it does not claim native `/settings` integration or monkey-patch internals.
|
|
51
|
+
|
|
52
|
+
## Verification
|
|
53
|
+
|
|
54
|
+
Unit tests cover values, explicit default behavior, immutable merging, malformed-body errors, endpoint/model-identity errors, CLI initialization, command errors/changes/reset, and non-UI startup. Optional installed-Pi integration tests use a dummy token and intercepted HTTP to check that the real OpenAI client serializes the added field, preserves a 403 denial without retrying, and makes zero HTTP calls for invalid configuration or payload. This verifies transport, **not server entitlement**.
|
package/package.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-daybreak-param",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Persistent /daybreak access-program selection for Pi's OpenAI and Sign in with ChatGPT requests",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "MercuriusDream",
|
|
8
|
+
"repository": { "type": "git", "url": "git+https://github.com/MercuriusDream/pi-daybreak-param.git" },
|
|
9
|
+
"homepage": "https://github.com/MercuriusDream/pi-daybreak-param#readme",
|
|
10
|
+
"bugs": { "url": "https://github.com/MercuriusDream/pi-daybreak-param/issues" },
|
|
11
|
+
"files": ["src/", "docs/", "README.md", "LICENSE"],
|
|
12
|
+
"publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" },
|
|
13
|
+
"keywords": ["pi-package"],
|
|
14
|
+
"pi": { "extensions": ["./src/index.ts"] },
|
|
15
|
+
"peerDependencies": { "@earendil-works/pi-coding-agent": "*", "@earendil-works/pi-ai": "*" },
|
|
16
|
+
"scripts": { "test": "bun test" }
|
|
17
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { parseMode, type CyberMode } from "./payload.ts";
|
|
5
|
+
|
|
6
|
+
export interface ModeStore {
|
|
7
|
+
read(): CyberMode;
|
|
8
|
+
write(mode: CyberMode): void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function fileModeStore(path: string): ModeStore {
|
|
12
|
+
return {
|
|
13
|
+
read() {
|
|
14
|
+
let text: string;
|
|
15
|
+
try {
|
|
16
|
+
text = readFileSync(path, "utf8");
|
|
17
|
+
} catch (error) {
|
|
18
|
+
// An absent configuration is an intentional initial state, not error recovery.
|
|
19
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return "default";
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
const value: unknown = JSON.parse(text);
|
|
23
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
24
|
+
throw new Error(`Invalid Daybreak configuration at ${path}: expected an object.`);
|
|
25
|
+
}
|
|
26
|
+
const config = value as Record<string, unknown>;
|
|
27
|
+
const mode = parseMode(config.cyber);
|
|
28
|
+
if (config.version !== 1 || !mode) {
|
|
29
|
+
throw new Error(`Invalid Daybreak configuration at ${path}: expected version 1 and a valid cyber selection.`);
|
|
30
|
+
}
|
|
31
|
+
return mode;
|
|
32
|
+
},
|
|
33
|
+
write(mode) {
|
|
34
|
+
if (!parseMode(mode)) throw new Error("Invalid Daybreak selection; configuration was not saved.");
|
|
35
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
36
|
+
const temp = `${path}.${randomUUID()}.tmp`;
|
|
37
|
+
try {
|
|
38
|
+
writeFileSync(temp, `${JSON.stringify({ version: 1, cyber: mode }, null, 2)}\n`, { mode: 0o600, flag: "wx" });
|
|
39
|
+
renameSync(temp, path);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
try {
|
|
42
|
+
unlinkSync(temp);
|
|
43
|
+
} catch (cleanupError) {
|
|
44
|
+
if ((cleanupError as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
45
|
+
throw new AggregateError([error, cleanupError], "Failed to save Daybreak configuration and clean up temporary file.");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { Api, SimpleStreamOptions, StreamFunction } from "@earendil-works/pi-ai";
|
|
3
|
+
import { isDirectOpenAI, MODES, parseMode, selectCyberProgram, type CyberMode } from "./payload.ts";
|
|
4
|
+
import type { ModeStore } from "./config.ts";
|
|
5
|
+
|
|
6
|
+
const LABELS: Record<CyberMode, string> = {
|
|
7
|
+
standard: "Standard — explicit Daybreak opt-out",
|
|
8
|
+
daybreak_blue: "Daybreak Blue — requires approved access",
|
|
9
|
+
daybreak_red: "Daybreak Red — requires approved access",
|
|
10
|
+
default: "Inherit — no override (not an opt-out)",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function registerDaybreak(pi: ExtensionAPI, streamOpenAI: StreamFunction<Api, SimpleStreamOptions>, store: ModeStore) {
|
|
14
|
+
pi.registerFlag("daybreak-cyber", {
|
|
15
|
+
type: "string",
|
|
16
|
+
description: `OpenAI access_programs.cyber request selection: ${MODES.join(", ")}. Requires approved access.`,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
let mode: CyberMode = "default";
|
|
20
|
+
let configError: Error | undefined = new Error("Daybreak configuration has not been initialized. OpenAI request blocked.");
|
|
21
|
+
const refreshStatus = (ctx: ExtensionContext) => {
|
|
22
|
+
if (ctx.hasUI) {
|
|
23
|
+
ctx.ui.setStatus("daybreak-param", configError ? "Cyber config: ERROR" :
|
|
24
|
+
mode !== "default" && isDirectOpenAI(ctx.model) ? `Cyber request: ${mode} (not verified)` : undefined);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
pi.on("session_start", (_event, ctx) => {
|
|
29
|
+
const flag = pi.getFlag("daybreak-cyber");
|
|
30
|
+
try {
|
|
31
|
+
// Always validate stored configuration: a flag must not hide a broken config file.
|
|
32
|
+
const saved = store.read();
|
|
33
|
+
if (flag === undefined) {
|
|
34
|
+
mode = saved;
|
|
35
|
+
} else {
|
|
36
|
+
const parsed = parseMode(flag);
|
|
37
|
+
if (!parsed) throw new Error(`Invalid --daybreak-cyber value: ${String(flag)}. Expected ${MODES.join(", ")}.`);
|
|
38
|
+
mode = parsed;
|
|
39
|
+
}
|
|
40
|
+
configError = undefined;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
configError = new Error(`Daybreak configuration error: ${error instanceof Error ? error.message : String(error)} OpenAI requests are blocked until corrected.`);
|
|
43
|
+
if (ctx.hasUI) ctx.ui.notify(configError.message, "error");
|
|
44
|
+
}
|
|
45
|
+
refreshStatus(ctx);
|
|
46
|
+
});
|
|
47
|
+
pi.on("model_select", (_event, ctx) => refreshStatus(ctx));
|
|
48
|
+
|
|
49
|
+
pi.registerCommand("daybreak", {
|
|
50
|
+
description: `Configure persistent cyber selection, or /daybreak status: ${MODES.join(" | ")}`,
|
|
51
|
+
handler: async (args, ctx) => {
|
|
52
|
+
let value = args.trim();
|
|
53
|
+
if (value === "status") {
|
|
54
|
+
ctx.ui.notify(configError?.message ?? `Cyber request selection: ${mode}. This is not confirmation of Daybreak access.`, configError ? "error" : "info");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
await ctx.waitForIdle();
|
|
58
|
+
if (!value) {
|
|
59
|
+
if (!ctx.hasUI) throw new Error("Use /daybreak <selection> or --daybreak-cyber in non-UI mode.");
|
|
60
|
+
const selected = await ctx.ui.select(`Cyber request selection: ${configError ? "CONFIG ERROR" : mode} (access not verified)`, Object.values(LABELS));
|
|
61
|
+
if (selected === undefined) return; // Explicit user cancellation changes nothing.
|
|
62
|
+
const entry = Object.entries(LABELS).find(([, label]) => label === selected);
|
|
63
|
+
if (!entry) throw new Error("Invalid Daybreak menu selection.");
|
|
64
|
+
value = entry[0];
|
|
65
|
+
}
|
|
66
|
+
const next = parseMode(value);
|
|
67
|
+
if (!next) throw new Error(`Usage: /daybreak ${MODES.join(" | ")}. Invalid selection: ${value}`);
|
|
68
|
+
try {
|
|
69
|
+
store.write(next);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
configError = new Error(`Failed to save Daybreak configuration: ${error instanceof Error ? error.message : String(error)} OpenAI requests are blocked until corrected.`);
|
|
72
|
+
refreshStatus(ctx);
|
|
73
|
+
throw configError;
|
|
74
|
+
}
|
|
75
|
+
mode = next;
|
|
76
|
+
configError = undefined;
|
|
77
|
+
refreshStatus(ctx);
|
|
78
|
+
ctx.ui.notify(next === "default"
|
|
79
|
+
? "Saved: inherit. Extension override disabled; existing payload and server defaults are unchanged."
|
|
80
|
+
: `Saved: request ${next} on OpenAI Responses calls. OpenAI still checks model compatibility and your approved access.`, "info");
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Preserve built-in models and BOTH authentication methods. Only delegate streaming.
|
|
85
|
+
// Unlike extension event handlers, exceptions in this onPayload callback terminate
|
|
86
|
+
// the provider stream before client.responses.create sends the HTTP request.
|
|
87
|
+
pi.registerProvider("openai", {
|
|
88
|
+
api: "openai-responses",
|
|
89
|
+
streamSimple(model, context, options) {
|
|
90
|
+
const requestMode = mode;
|
|
91
|
+
const requestError = configError;
|
|
92
|
+
return streamOpenAI(model, context, {
|
|
93
|
+
...options,
|
|
94
|
+
onPayload: async (payload, requestModel) => {
|
|
95
|
+
if (requestError) throw requestError;
|
|
96
|
+
const prior = await options?.onPayload?.(payload, requestModel);
|
|
97
|
+
const current = prior === undefined ? payload : prior;
|
|
98
|
+
const replacement = selectCyberProgram(current, requestModel, requestMode);
|
|
99
|
+
return replacement === undefined ? current : replacement;
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { openAIResponsesApi } from "@earendil-works/pi-ai/compat";
|
|
4
|
+
import { registerDaybreak } from "./controller.ts";
|
|
5
|
+
import { fileModeStore } from "./config.ts";
|
|
6
|
+
|
|
7
|
+
export default function daybreak(pi: ExtensionAPI) {
|
|
8
|
+
registerDaybreak(pi, openAIResponsesApi().streamSimple, fileModeStore(join(getAgentDir(), "daybreak.json")));
|
|
9
|
+
}
|
package/src/payload.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export const MODES = ["default", "standard", "daybreak_blue", "daybreak_red"] as const;
|
|
2
|
+
export type CyberMode = (typeof MODES)[number];
|
|
3
|
+
|
|
4
|
+
export function parseMode(value: unknown): CyberMode | undefined {
|
|
5
|
+
return typeof value === "string" && MODES.includes(value as CyberMode)
|
|
6
|
+
? (value as CyberMode)
|
|
7
|
+
: undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
11
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface RequestModel {
|
|
15
|
+
provider: string;
|
|
16
|
+
api: string;
|
|
17
|
+
id: string;
|
|
18
|
+
baseUrl: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function isDirectOpenAI(model: RequestModel | undefined): boolean {
|
|
22
|
+
// Match Pi's own SIWC detection exactly. Similar-looking URLs (including a
|
|
23
|
+
// trailing slash, query, fragment, or userinfo) do not use the same code path.
|
|
24
|
+
return model?.provider === "openai" && model.api === "openai-responses"
|
|
25
|
+
&& model.baseUrl === "https://api.openai.com/v1";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Return the replacement body itself, not { payload: ... }. Never mutate other handlers' objects. */
|
|
29
|
+
export function selectCyberProgram(
|
|
30
|
+
payload: unknown,
|
|
31
|
+
model: RequestModel | undefined,
|
|
32
|
+
mode: CyberMode,
|
|
33
|
+
): Record<string, unknown> | undefined {
|
|
34
|
+
// Default is a no-op, preserving Pi/server defaults and other extensions' choices.
|
|
35
|
+
if (mode === "default") return undefined;
|
|
36
|
+
if (!isDirectOpenAI(model)) throw new Error("Daybreak selection requires the direct OpenAI Responses endpoint.");
|
|
37
|
+
if (!isRecord(payload)) throw new Error("OpenAI request payload must be an object.");
|
|
38
|
+
if (payload.model !== model!.id) throw new Error("OpenAI request model does not match the selected request model.");
|
|
39
|
+
// Invalid values are errors, not a reason to silently send an unmodified request.
|
|
40
|
+
if (payload.access_programs !== undefined && !isRecord(payload.access_programs)) {
|
|
41
|
+
throw new Error("access_programs must be an object (not null).");
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
...payload,
|
|
45
|
+
access_programs: { ...(payload.access_programs as Record<string, unknown> | undefined), cyber: mode },
|
|
46
|
+
};
|
|
47
|
+
}
|