@oai404iao/pi-codex-minimal-tools 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/LICENSE +28 -0
  2. package/LICENSES/Apache-2.0.txt +201 -0
  3. package/LICENSES/OpenAI-Codex-NOTICE.txt +6 -0
  4. package/README.md +410 -0
  5. package/THIRD_PARTY_NOTICES.md +97 -0
  6. package/config.schema.json +174 -0
  7. package/models.schema.json +217 -0
  8. package/package.json +87 -0
  9. package/provenance/openai-codex-eb9dceba-reserved-tools.json +140 -0
  10. package/src/activation.ts +56 -0
  11. package/src/background-image-generation.ts +574 -0
  12. package/src/capabilities.ts +146 -0
  13. package/src/codex-http.ts +133 -0
  14. package/src/codex-request-profile.ts +45 -0
  15. package/src/codex-reserved-tools.ts +323 -0
  16. package/src/codex-wire-identity.ts +182 -0
  17. package/src/fast-mode.ts +124 -0
  18. package/src/glyphs.ts +70 -0
  19. package/src/index.ts +332 -0
  20. package/src/model-catalog/catalog.ts +636 -0
  21. package/src/model-catalog/default-models.json +252 -0
  22. package/src/model-catalog/runtime.ts +113 -0
  23. package/src/model-catalog/types.ts +95 -0
  24. package/src/native-compaction.ts +393 -0
  25. package/src/patch/apply.ts +338 -0
  26. package/src/patch/parser.ts +224 -0
  27. package/src/patch/render.ts +201 -0
  28. package/src/provider-headers.ts +54 -0
  29. package/src/provider-native-tools.ts +71 -0
  30. package/src/provider-shim.ts +4338 -0
  31. package/src/providers/codex-apply-patch-tool.ts +23 -0
  32. package/src/providers/codex-apply-patch.lark +19 -0
  33. package/src/providers/openai-responses-shared.ts +1463 -0
  34. package/src/settings.ts +247 -0
  35. package/src/tools/apply-patch.ts +84 -0
  36. package/src/tools/image-generation.ts +274 -0
  37. package/src/tools/view-image.ts +98 -0
  38. package/src/tools/web-search.ts +524 -0
  39. package/src/utils/images.ts +73 -0
@@ -0,0 +1,182 @@
1
+ //! Codex wire identity: UUID v7 session/thread/window identifiers and the
2
+ //! sticky-routing turn-state token, matching the Codex CLI's request identity.
3
+ //!
4
+ //! Pi's own session ids are short opaque strings (often 8 hex chars). The
5
+ //! Codex ChatGPT backend expects real UUID-shaped ids in `session-id`,
6
+ //! `thread-id`, `x-codex-window-id`, `prompt_cache_key`, and
7
+ //! `client_metadata`; the CLI generates UUID v7 values. This module derives a
8
+ //! stable UUID v7 identity per (pi session, thread) pair and remembers the
9
+ //! `x-codex-turn-state` sticky-routing token emitted by the backend so later
10
+ //! requests in the same session can replay it.
11
+
12
+ const WIRE_IDENTITY_CACHE_LIMIT = 1024;
13
+
14
+ export interface CodexWireIdentity {
15
+ /** Stable UUID v7 for the pi session. */
16
+ sessionId: string;
17
+ /** Stable UUID v7 for the thread (defaults to a per-thread value). */
18
+ threadId: string;
19
+ /** UUID v7 for the current auto-compact window; rotated on compaction. */
20
+ windowId: string;
21
+ }
22
+
23
+ interface CachedThread {
24
+ threadId: string;
25
+ windowId: string;
26
+ }
27
+
28
+ interface CachedSession {
29
+ sessionId: string;
30
+ threads: Map<string, CachedThread>;
31
+ }
32
+
33
+ const sessionCache = new Map<string, CachedSession>();
34
+ const turnStateCache = new Map<string, string>();
35
+ const installationIdCache = new Map<string, string>();
36
+
37
+ function randomBytes(count: number): Uint8Array {
38
+ if (typeof globalThis.crypto?.getRandomValues === "function") {
39
+ const bytes = new Uint8Array(count);
40
+ globalThis.crypto.getRandomValues(bytes);
41
+ return bytes;
42
+ }
43
+ const bytes = new Uint8Array(count);
44
+ for (let index = 0; index < count; index++) {
45
+ bytes[index] = Math.floor(Math.random() * 256);
46
+ }
47
+ return bytes;
48
+ }
49
+
50
+ function uuidBytesToHex(bytes: Uint8Array): string {
51
+ const hex: string[] = [];
52
+ for (const byte of bytes) {
53
+ hex.push(byte.toString(16).padStart(2, "0"));
54
+ }
55
+ return [
56
+ hex.slice(0, 4).join(""),
57
+ hex.slice(4, 6).join(""),
58
+ hex.slice(6, 8).join(""),
59
+ hex.slice(8, 10).join(""),
60
+ hex.slice(10, 16).join(""),
61
+ ].join("-");
62
+ }
63
+
64
+ /** RFC 9562 UUID v7: 48-bit millisecond timestamp + random, version 7, variant RFC 4122. */
65
+ export function uuidV7(): string {
66
+ const bytes = randomBytes(16);
67
+ const timestamp = Date.now();
68
+ bytes[0] = (timestamp / 0x10000000000) & 0xff;
69
+ bytes[1] = (timestamp / 0x100000000) & 0xff;
70
+ bytes[2] = (timestamp / 0x1000000) & 0xff;
71
+ bytes[3] = (timestamp / 0x10000) & 0xff;
72
+ bytes[4] = (timestamp / 0x100) & 0xff;
73
+ bytes[5] = timestamp & 0xff;
74
+ bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x70;
75
+ bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80;
76
+ return uuidBytesToHex(bytes);
77
+ }
78
+
79
+ function uuidV4(): string {
80
+ const bytes = randomBytes(16);
81
+ bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x40;
82
+ bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80;
83
+ return uuidBytesToHex(bytes);
84
+ }
85
+
86
+ /**
87
+ * Stable per-session installation id (UUID v4), mirroring the CLI's
88
+ * `installation_id` file: one opaque id per installation that never changes
89
+ * for the lifetime of the session.
90
+ */
91
+ export function codexInstallationIdFor(sessionKey: string): string {
92
+ let id = installationIdCache.get(sessionKey);
93
+ if (!id) {
94
+ id = typeof globalThis.crypto?.randomUUID === "function"
95
+ ? globalThis.crypto.randomUUID()
96
+ : uuidV4();
97
+ installationIdCache.set(sessionKey, id);
98
+ }
99
+ return id;
100
+ }
101
+
102
+ function newCachedSession(): CachedSession {
103
+ return {
104
+ sessionId: uuidV7(),
105
+ threads: new Map(),
106
+ };
107
+ }
108
+
109
+ function ensureSession(sessionKey: string): CachedSession {
110
+ let session = sessionCache.get(sessionKey);
111
+ if (!session) {
112
+ if (sessionCache.size >= WIRE_IDENTITY_CACHE_LIMIT) {
113
+ sessionCache.clear();
114
+ turnStateCache.clear();
115
+ }
116
+ session = newCachedSession();
117
+ sessionCache.set(sessionKey, session);
118
+ }
119
+ return session;
120
+ }
121
+
122
+ function ensureThread(session: CachedSession, threadKey: string): CachedThread {
123
+ let thread = session.threads.get(threadKey);
124
+ if (!thread) {
125
+ thread = { threadId: uuidV7(), windowId: uuidV7() };
126
+ session.threads.set(threadKey, thread);
127
+ }
128
+ return thread;
129
+ }
130
+
131
+ /**
132
+ * Resolve the stable UUID v7 wire identity for a (session, thread) pair.
133
+ * Values are generated once and reused for the lifetime of the pair so the
134
+ * backend sees consistent `session-id`/`thread-id`/`x-codex-window-id` values,
135
+ * exactly like the Codex CLI.
136
+ */
137
+ export function resolveCodexWireIdentity(
138
+ sessionKey: string,
139
+ threadKey?: string,
140
+ ): CodexWireIdentity {
141
+ const session = ensureSession(sessionKey);
142
+ const thread = ensureThread(session, threadKey ?? sessionKey);
143
+ return {
144
+ sessionId: session.sessionId,
145
+ threadId: thread.threadId,
146
+ windowId: thread.windowId,
147
+ };
148
+ }
149
+
150
+ /**
151
+ * Rotate the auto-compact window id for a (session, thread) pair. Session and
152
+ * thread ids stay stable; only `x-codex-window-id` changes, mirroring the
153
+ * CLI's auto-compact window lifecycle.
154
+ */
155
+ export function rotateCodexWindowId(sessionKey: string, threadKey?: string): void {
156
+ const session = ensureSession(sessionKey);
157
+ const thread = ensureThread(session, threadKey ?? sessionKey);
158
+ thread.windowId = uuidV7();
159
+ }
160
+
161
+ /** Current sticky-routing turn-state token for a session, if the backend issued one. */
162
+ export function codexTurnStateFor(sessionKey: string): string | undefined {
163
+ return turnStateCache.get(sessionKey);
164
+ }
165
+
166
+ /** Remember the sticky-routing token emitted by the backend. */
167
+ export function captureCodexTurnState(sessionKey: string, token: string | undefined): void {
168
+ if (!sessionKey || typeof token !== "string" || !token.trim()) return;
169
+ turnStateCache.set(sessionKey, token.trim());
170
+ }
171
+
172
+ /** Forget all derived identity and turn-state state (tests, session teardown). */
173
+ export function resetCodexWireState(): void {
174
+ sessionCache.clear();
175
+ turnStateCache.clear();
176
+ installationIdCache.clear();
177
+ }
178
+
179
+ /** Current number of cached identities (diagnostics/tests). */
180
+ export function codexWireIdentityCount(): number {
181
+ return sessionCache.size;
182
+ }
@@ -0,0 +1,124 @@
1
+ import type {
2
+ ExtensionAPI,
3
+ ExtensionCommandContext,
4
+ ExtensionContext,
5
+ } from "@earendil-works/pi-coding-agent";
6
+ import {
7
+ modelKey,
8
+ type ModelLike,
9
+ } from "./capabilities.js";
10
+ import { loadModelSettings } from "./model-catalog/runtime.js";
11
+ import type { ResolvedCodexModelSettings } from "./model-catalog/runtime.js";
12
+ import {
13
+ configPath,
14
+ loadSettings,
15
+ updateConfig,
16
+ type CodexMinimalToolsSettings,
17
+ } from "./settings.js";
18
+
19
+ export const FAST_MODE_STATUS_KEY = "codex-fast-mode";
20
+ export const FAST_MODE_SERVICE_TIER = "priority" as const;
21
+
22
+ export function isFastModeModel(model: ModelLike | undefined): boolean {
23
+ return Boolean(loadModelSettings(model).fastServiceTier);
24
+ }
25
+
26
+ export function resolveFastModeServiceTier(
27
+ settings: Pick<CodexMinimalToolsSettings, "enabled" | "fastMode"> & Partial<ResolvedCodexModelSettings>,
28
+ model: ModelLike | undefined,
29
+ ): string | undefined {
30
+ if (!settings.enabled || !settings.fastMode) return undefined;
31
+ if (settings.modelProfile) return settings.fastServiceTier;
32
+ return loadModelSettings(model, undefined, settings as CodexMinimalToolsSettings).fastServiceTier;
33
+ }
34
+
35
+ export function applyFastModeServiceTier<T extends Record<string, unknown>>(
36
+ body: T,
37
+ settings: Pick<CodexMinimalToolsSettings, "enabled" | "fastMode"> & Partial<ResolvedCodexModelSettings>,
38
+ model: ModelLike | undefined,
39
+ ): T {
40
+ const serviceTier = resolveFastModeServiceTier(settings, model);
41
+ if (!serviceTier || body.service_tier !== undefined) return body;
42
+ return { ...body, service_tier: serviceTier };
43
+ }
44
+
45
+ function fastModeLines(ctx: ExtensionContext): string[] {
46
+ const settings = loadSettings(ctx.cwd);
47
+ const model = ctx.model as ModelLike | undefined;
48
+ const modelSettings = loadModelSettings(model, ctx.cwd, settings);
49
+ const activeTier = resolveFastModeServiceTier(modelSettings, model);
50
+ return [
51
+ "Codex Fast mode",
52
+ `enabled: ${settings.fastMode}`,
53
+ `service tier: ${modelSettings.fastServiceTier ?? "(unsupported)"}`,
54
+ `model: ${modelKey(model)}`,
55
+ `active for current model: ${Boolean(activeTier)}`,
56
+ `config: ${configPath()}`,
57
+ ];
58
+ }
59
+
60
+ export function syncFastModeStatus(ctx: ExtensionContext): void {
61
+ const settings = loadSettings(ctx.cwd);
62
+ const model = ctx.model as ModelLike | undefined;
63
+ const tier = resolveFastModeServiceTier(loadModelSettings(model, ctx.cwd, settings), model);
64
+ const ui = ctx.ui as ExtensionContext["ui"] | undefined;
65
+ ui?.setStatus?.(
66
+ FAST_MODE_STATUS_KEY,
67
+ tier,
68
+ );
69
+ }
70
+
71
+ function showFastModeStatus(ctx: ExtensionCommandContext): void {
72
+ syncFastModeStatus(ctx as ExtensionContext);
73
+ ctx.ui.notify(fastModeLines(ctx as ExtensionContext).join("\n"), "info");
74
+ }
75
+
76
+ export function registerFastMode(pi: ExtensionAPI): void {
77
+ pi.registerCommand("fast", {
78
+ description: "Toggle model-profile Fast mode. Usage: /fast [on|off|status]",
79
+ handler: async (args: string, ctx) => {
80
+ const command = args.trim().toLowerCase().split(/\s+/, 1)[0] ?? "";
81
+ const settings = loadSettings(ctx.cwd);
82
+ let patch: Partial<CodexMinimalToolsSettings> | undefined;
83
+
84
+ switch (command) {
85
+ case "":
86
+ patch = { fastMode: !settings.fastMode };
87
+ break;
88
+ case "on":
89
+ patch = { fastMode: true };
90
+ break;
91
+ case "off":
92
+ patch = { fastMode: false };
93
+ break;
94
+ case "status":
95
+ showFastModeStatus(ctx);
96
+ return;
97
+ default:
98
+ ctx.ui.notify("Usage: /fast [on|off|status]", "warning");
99
+ return;
100
+ }
101
+
102
+ try {
103
+ updateConfig(patch);
104
+ showFastModeStatus(ctx);
105
+ } catch (error) {
106
+ ctx.ui.notify(
107
+ `Failed to update Fast mode: ${error instanceof Error ? error.message : String(error)}`,
108
+ "error",
109
+ );
110
+ }
111
+ },
112
+ });
113
+
114
+ pi.on("session_start", async (_event, ctx) => {
115
+ syncFastModeStatus(ctx);
116
+ });
117
+ pi.on("model_select", async (_event, ctx) => {
118
+ syncFastModeStatus(ctx);
119
+ });
120
+ pi.on("session_shutdown", async (_event, ctx) => {
121
+ const ui = ctx.ui as ExtensionContext["ui"] | undefined;
122
+ ui?.setStatus?.(FAST_MODE_STATUS_KEY, undefined);
123
+ });
124
+ }
package/src/glyphs.ts ADDED
@@ -0,0 +1,70 @@
1
+ import { loadSettings } from "./settings.js";
2
+
3
+ export type GlyphStyle = "unicode" | "ascii";
4
+
5
+ export function glyphStyle(cwd?: string): GlyphStyle {
6
+ return loadSettings(cwd).glyphStyle;
7
+ }
8
+
9
+ export const GLYPHS = {
10
+ unicode: {
11
+ frame: { tl: "┏", tr: "┓", bl: "┗", br: "┛", h: "━", v: "┃" },
12
+ line: "─",
13
+ tree: { mid: "├─ ", last: "└─ ", stem: "│ ", blank: " " },
14
+ bullet: "● ",
15
+ emptyBullet: "○ ",
16
+ dot: " · ",
17
+ ok: "✓",
18
+ fail: "✗",
19
+ warn: "▲",
20
+ diamond: "◆",
21
+ prompt: "π",
22
+ ellipsis: "…",
23
+ arrow: "→",
24
+ codeBar: "▌",
25
+ },
26
+ ascii: {
27
+ frame: { tl: "+", tr: "+", bl: "+", br: "+", h: "-", v: "|" },
28
+ line: "-",
29
+ tree: { mid: "|-- ", last: "`-- ", stem: "| ", blank: " " },
30
+ bullet: "* ",
31
+ emptyBullet: "o ",
32
+ dot: " - ",
33
+ ok: "+",
34
+ fail: "x",
35
+ warn: "!",
36
+ diamond: "*",
37
+ prompt: "pi",
38
+ ellipsis: "...",
39
+ arrow: "->",
40
+ codeBar: "|",
41
+ },
42
+ } as const;
43
+
44
+ export function glyphs(cwd?: string): (typeof GLYPHS)[GlyphStyle] {
45
+ return GLYPHS[glyphStyle(cwd)];
46
+ }
47
+
48
+ export function truncateIndicator(cwd?: string): string {
49
+ return glyphs(cwd).ellipsis;
50
+ }
51
+
52
+ export function truncateText(text: string, maxChars: number, cwd?: string): string {
53
+ if (text.length <= maxChars) return text;
54
+ const indicator = truncateIndicator(cwd);
55
+ return `${text.slice(0, Math.max(0, maxChars - indicator.length))}${indicator}`;
56
+ }
57
+
58
+ export function dot(cwd?: string): string {
59
+ return glyphs(cwd).dot;
60
+ }
61
+
62
+ export function treeGlyph(branch: "├" | "└" | "│", cwd?: string): string {
63
+ const tree = glyphs(cwd).tree;
64
+ if (branch === "│") return tree.stem;
65
+ return branch === "└" ? tree.last : tree.mid;
66
+ }
67
+
68
+ export function frameGlyphs(cwd?: string): (typeof GLYPHS)[GlyphStyle]["frame"] {
69
+ return glyphs(cwd).frame;
70
+ }