@wlv-zedd/dsh-chatgpt-web 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +140 -0
- package/assets/demo.gif +0 -0
- package/assets/hero-demo.png +0 -0
- package/assets/promo-dshmarket-official.png +0 -0
- package/cordis.patch.yml +4 -0
- package/lib/cli.js +239642 -0
- package/lib/plugin.js +195 -0
- package/package.json +88 -0
- package/screenshots.json +5 -0
- package/src/adapters/base.ts +16 -0
- package/src/adapters/chatgpt-web/adapter-error.ts +59 -0
- package/src/adapters/chatgpt-web/browser-helper-main.ts +513 -0
- package/src/adapters/chatgpt-web/browser-helper-prompt-selection.ts +27 -0
- package/src/adapters/chatgpt-web/browser-worker.ts +4944 -0
- package/src/adapters/chatgpt-web/codex-rollout-environment.ts +628 -0
- package/src/adapters/chatgpt-web/compaction-handoff.ts +533 -0
- package/src/adapters/chatgpt-web/compaction-transaction.ts +142 -0
- package/src/adapters/chatgpt-web/concurrency.ts +6 -0
- package/src/adapters/chatgpt-web/conversation-key.ts +58 -0
- package/src/adapters/chatgpt-web/environment.ts +669 -0
- package/src/adapters/chatgpt-web/index.ts +1544 -0
- package/src/adapters/chatgpt-web/input-tokens.ts +74 -0
- package/src/adapters/chatgpt-web/launcher-helper-client.ts +695 -0
- package/src/adapters/chatgpt-web/markdown.ts +418 -0
- package/src/adapters/chatgpt-web/mcp-main.ts +25 -0
- package/src/adapters/chatgpt-web/mcp-server.ts +933 -0
- package/src/adapters/chatgpt-web/model.ts +70 -0
- package/src/adapters/chatgpt-web/native-compaction-control.ts +74 -0
- package/src/adapters/chatgpt-web/output-validation.ts +62 -0
- package/src/adapters/chatgpt-web/process-line-writer.ts +46 -0
- package/src/adapters/chatgpt-web/prompt.ts +702 -0
- package/src/adapters/chatgpt-web/retry-policy.ts +73 -0
- package/src/adapters/chatgpt-web/rolling-checkpoint.ts +384 -0
- package/src/adapters/chatgpt-web/thread-environment.ts +238 -0
- package/src/adapters/chatgpt-web/tool-stream-parser.ts +601 -0
- package/src/adapters/chatgpt-web/turn-broker.ts +1481 -0
- package/src/adapters/chatgpt-web/turn-execution.ts +816 -0
- package/src/adapters/chatgpt-web/turn-progress.ts +292 -0
- package/src/adapters/chatgpt-web/usage.ts +121 -0
- package/src/adapters/image.ts +9 -0
- package/src/bridge.ts +1083 -0
- package/src/browser-login.ts +521 -0
- package/src/chatgpt-session.ts +240 -0
- package/src/chatgpt-web-models.ts +400 -0
- package/src/cli.ts +568 -0
- package/src/codex-integration-document.ts +824 -0
- package/src/codex-integration-journal.ts +212 -0
- package/src/codex-integration-route.ts +515 -0
- package/src/codex-integration-shared.ts +332 -0
- package/src/codex-integration.ts +529 -0
- package/src/codex-interrupt-hook.ts +158 -0
- package/src/config.ts +616 -0
- package/src/dev-chat/cli.ts +432 -0
- package/src/dev-chat/constants.ts +3 -0
- package/src/dev-chat/driver.ts +655 -0
- package/src/dev-chat/profile.ts +223 -0
- package/src/dev-chat/session.ts +287 -0
- package/src/dev-chat/transport.ts +54 -0
- package/src/doctor.ts +237 -0
- package/src/event-queue.ts +45 -0
- package/src/http-body.ts +30 -0
- package/src/launcher-browser-host.ts +695 -0
- package/src/lib/errors.ts +281 -0
- package/src/lib/token-estimate.ts +42 -0
- package/src/login-helper.cjs +140 -0
- package/src/model-catalog.ts +197 -0
- package/src/native-passthrough.ts +261 -0
- package/src/plugin.ts +191 -0
- package/src/process.ts +45 -0
- package/src/responses/compaction.ts +199 -0
- package/src/responses/parser.ts +633 -0
- package/src/responses/reasoning-envelope.ts +49 -0
- package/src/responses/schema.ts +172 -0
- package/src/responses/state.ts +230 -0
- package/src/server.ts +1111 -0
- package/src/service.ts +315 -0
- package/src/setup.ts +671 -0
- package/src/stall-timeout.ts +23 -0
- package/src/tunnel-service.ts +160 -0
- package/src/tunnel.ts +417 -0
- package/src/turndown-plugin-gfm.d.ts +5 -0
- package/src/types.ts +307 -0
- package/src/usage/totals.ts +12 -0
- package/src/version.ts +1 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
2
|
+
import { readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { isAbsolute, join, posix, resolve, win32 } from "node:path";
|
|
5
|
+
import { expandUserPath, getConfigPath } from "../config";
|
|
6
|
+
import {
|
|
7
|
+
readLauncherBrowserHostDescriptor,
|
|
8
|
+
type LauncherBrowserHostDescriptor,
|
|
9
|
+
} from "../launcher-browser-host";
|
|
10
|
+
|
|
11
|
+
import { DEV_LAUNCHER_PROFILE } from "./constants";
|
|
12
|
+
|
|
13
|
+
export interface DevProfilePaths {
|
|
14
|
+
home: string;
|
|
15
|
+
codexHome: string;
|
|
16
|
+
launcherUserData: string;
|
|
17
|
+
launcherStatePath: string;
|
|
18
|
+
descriptorPath: string;
|
|
19
|
+
chatsPath: string;
|
|
20
|
+
runtimePath: string;
|
|
21
|
+
configPath: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const WINDOWS_LAUNCHER_GUID = "d1a6026a-6210-588e-9a2b-da3936f94e02";
|
|
25
|
+
|
|
26
|
+
function registeredWindowsLauncherInstallLocation(): string | undefined {
|
|
27
|
+
try {
|
|
28
|
+
const output = execFileSync(
|
|
29
|
+
"reg.exe",
|
|
30
|
+
["query", `HKCU\\Software\\${WINDOWS_LAUNCHER_GUID}`, "/v", "InstallLocation"],
|
|
31
|
+
{ encoding: "utf8", windowsHide: true },
|
|
32
|
+
);
|
|
33
|
+
const match = output.match(/^\s*InstallLocation\s+REG_SZ\s+(.+?)\s*$/mi);
|
|
34
|
+
return match && win32.isAbsolute(match[1]) ? match[1] : undefined;
|
|
35
|
+
} catch {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function resolveDevProfilePaths({
|
|
41
|
+
environment = process.env,
|
|
42
|
+
homeDirectory = homedir(),
|
|
43
|
+
}: {
|
|
44
|
+
environment?: NodeJS.ProcessEnv;
|
|
45
|
+
homeDirectory?: string;
|
|
46
|
+
} = {}): DevProfilePaths {
|
|
47
|
+
const home = resolve(expandUserPath(
|
|
48
|
+
environment.DSH_CHATGPT_FREE_DEV_HOME?.trim()
|
|
49
|
+
|| join(homeDirectory, ".dsh", "storages", "chatgpt-web-dev"),
|
|
50
|
+
));
|
|
51
|
+
const productionHome = resolve(expandUserPath(
|
|
52
|
+
environment.DSH_CHATGPT_FREE_HOME?.trim()
|
|
53
|
+
|| environment.DSH_CHATGPT_WEB_HOME?.trim()
|
|
54
|
+
|| join(homeDirectory, ".dsh", "storages", "chatgpt-web"),
|
|
55
|
+
));
|
|
56
|
+
if (home === productionHome) {
|
|
57
|
+
throw new Error("DEV profile home must differ from the production home");
|
|
58
|
+
}
|
|
59
|
+
const launcherUserData = join(home, "launcher");
|
|
60
|
+
return {
|
|
61
|
+
home,
|
|
62
|
+
codexHome: join(home, "codex-home"),
|
|
63
|
+
launcherUserData,
|
|
64
|
+
launcherStatePath: join(launcherUserData, "launcher-state.json"),
|
|
65
|
+
descriptorPath: join(home, "runtime", "launcher-browser.json"),
|
|
66
|
+
chatsPath: join(home, "chats"),
|
|
67
|
+
runtimePath: join(home, "runtime", "dev-chat"),
|
|
68
|
+
configPath: join(home, "config.json"),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface DevChatExperimentalFeatures {
|
|
73
|
+
biggerContext: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Read the canonical DEV runtime setting consumed by repository chat commands. */
|
|
77
|
+
export function readDevChatExperimentalFeatures(
|
|
78
|
+
paths = resolveDevProfilePaths(),
|
|
79
|
+
): DevChatExperimentalFeatures {
|
|
80
|
+
let value: unknown;
|
|
81
|
+
try {
|
|
82
|
+
value = JSON.parse(readFileSync(paths.configPath, "utf8"));
|
|
83
|
+
} catch (error) {
|
|
84
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return { biggerContext: false };
|
|
85
|
+
throw new Error(
|
|
86
|
+
`Could not read DEV runtime settings from ${paths.configPath}: ${error instanceof Error ? error.message : String(error)}`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || (value as { version?: unknown }).version !== 3) {
|
|
90
|
+
throw new Error(`Invalid DEV runtime settings in ${paths.configPath}`);
|
|
91
|
+
}
|
|
92
|
+
const enabled = (value as { experimentalBiggerContext?: unknown }).experimentalBiggerContext;
|
|
93
|
+
if (enabled !== undefined && typeof enabled !== "boolean") {
|
|
94
|
+
throw new Error(`Invalid Bigger Context preference in ${paths.configPath}`);
|
|
95
|
+
}
|
|
96
|
+
return { biggerContext: enabled === true };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function activateDevProfileEnvironment(paths = resolveDevProfilePaths()): DevProfilePaths {
|
|
100
|
+
process.env.DSH_CHATGPT_FREE_DEV_HOME = paths.home;
|
|
101
|
+
process.env.DSH_CHATGPT_FREE_HOME = paths.home;
|
|
102
|
+
if (getConfigPath() !== paths.configPath) {
|
|
103
|
+
throw new Error("DEV profile environment did not resolve to its isolated configuration path");
|
|
104
|
+
}
|
|
105
|
+
return paths;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function executableFile(path: string): boolean {
|
|
109
|
+
try {
|
|
110
|
+
const stat = statSync(path);
|
|
111
|
+
return stat.isFile() && (process.platform === "win32" || (stat.mode & 0o111) !== 0);
|
|
112
|
+
} catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function installedLauncherCandidates({
|
|
118
|
+
environment = process.env,
|
|
119
|
+
homeDirectory = homedir(),
|
|
120
|
+
platform = process.platform,
|
|
121
|
+
windowsInstallLocation,
|
|
122
|
+
}: {
|
|
123
|
+
environment?: NodeJS.ProcessEnv;
|
|
124
|
+
homeDirectory?: string;
|
|
125
|
+
platform?: NodeJS.Platform;
|
|
126
|
+
windowsInstallLocation?: string;
|
|
127
|
+
} = {}): string[] {
|
|
128
|
+
const override = environment.CODEX_WEB_GPT_LAUNCHER_EXECUTABLE?.trim();
|
|
129
|
+
const candidates = override ? [expandUserPath(override)] : [];
|
|
130
|
+
const targetPath = platform === "win32" ? win32 : posix;
|
|
131
|
+
if (platform === "darwin") {
|
|
132
|
+
candidates.push(
|
|
133
|
+
"/Applications/Codex Web GPT.app/Contents/MacOS/Codex Web GPT",
|
|
134
|
+
posix.join(homeDirectory, "Applications", "Codex Web GPT.app", "Contents", "MacOS", "Codex Web GPT"),
|
|
135
|
+
);
|
|
136
|
+
} else if (platform === "win32") {
|
|
137
|
+
const registeredLocation = windowsInstallLocation?.trim()
|
|
138
|
+
|| (process.platform === "win32" ? registeredWindowsLauncherInstallLocation() : undefined);
|
|
139
|
+
if (registeredLocation && win32.isAbsolute(registeredLocation)) {
|
|
140
|
+
candidates.push(win32.join(registeredLocation, "Codex Web GPT.exe"));
|
|
141
|
+
} else {
|
|
142
|
+
const localAppData = environment.LOCALAPPDATA?.trim();
|
|
143
|
+
if (localAppData) {
|
|
144
|
+
candidates.push(win32.join(localAppData, "Programs", "Codex Web GPT", "Codex Web GPT.exe"));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
} else if (platform === "linux") {
|
|
148
|
+
candidates.push(posix.join(homeDirectory, ".local", "bin", "codex-web-gpt"));
|
|
149
|
+
for (const entry of (environment.PATH || "").split(":").filter(Boolean)) {
|
|
150
|
+
candidates.push(posix.join(entry, "codex-web-gpt"));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return [...new Set(candidates.map(candidate => targetPath.resolve(candidate)))];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function findInstalledLauncherExecutable(options: Parameters<typeof installedLauncherCandidates>[0] = {}): string {
|
|
157
|
+
const candidates = installedLauncherCandidates(options);
|
|
158
|
+
const executable = candidates.find(executableFile);
|
|
159
|
+
if (executable) return executable;
|
|
160
|
+
throw new Error(
|
|
161
|
+
"Installed Codex Web GPT launcher was not found. Install it first or set CODEX_WEB_GPT_LAUNCHER_EXECUTABLE to its absolute executable path."
|
|
162
|
+
+ ` Checked: ${candidates.join(", ") || "no platform candidates"}`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function devLauncherEnvironment(
|
|
167
|
+
paths: DevProfilePaths,
|
|
168
|
+
environment: NodeJS.ProcessEnv = process.env,
|
|
169
|
+
): NodeJS.ProcessEnv {
|
|
170
|
+
const childEnvironment = { ...environment };
|
|
171
|
+
delete childEnvironment.DSH_CHATGPT_FREE_HOME;
|
|
172
|
+
delete childEnvironment.DSH_CHATGPT_WEB_HOME;
|
|
173
|
+
childEnvironment.DSH_CHATGPT_FREE_DEV_HOME = paths.home;
|
|
174
|
+
return childEnvironment;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function devDescriptor(path: string): LauncherBrowserHostDescriptor {
|
|
178
|
+
const descriptor = readLauncherBrowserHostDescriptor(path);
|
|
179
|
+
if (descriptor.profile !== DEV_LAUNCHER_PROFILE) {
|
|
180
|
+
throw new Error(`Launcher descriptor belongs to ${descriptor.profile}, not the isolated DEV profile`);
|
|
181
|
+
}
|
|
182
|
+
return descriptor;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export async function waitForDevLauncher(
|
|
186
|
+
descriptorPath: string,
|
|
187
|
+
timeoutMs = 30_000,
|
|
188
|
+
): Promise<LauncherBrowserHostDescriptor> {
|
|
189
|
+
const deadline = Date.now() + timeoutMs;
|
|
190
|
+
let lastError = "descriptor is not ready";
|
|
191
|
+
while (Date.now() < deadline) {
|
|
192
|
+
try {
|
|
193
|
+
return devDescriptor(descriptorPath);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
196
|
+
}
|
|
197
|
+
await new Promise(resolveWait => setTimeout(resolveWait, 100));
|
|
198
|
+
}
|
|
199
|
+
throw new Error(`DEV launcher did not become ready within ${timeoutMs}ms: ${lastError}`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export async function launchDevProfile(
|
|
203
|
+
paths = resolveDevProfilePaths(),
|
|
204
|
+
options: { executable?: string; timeoutMs?: number } = {},
|
|
205
|
+
): Promise<{ descriptor: LauncherBrowserHostDescriptor; executable: string; alreadyRunning: boolean }> {
|
|
206
|
+
let existing: LauncherBrowserHostDescriptor | undefined;
|
|
207
|
+
try { existing = devDescriptor(paths.descriptorPath); }
|
|
208
|
+
catch { /* A stale or absent descriptor is replaced only by its owning launcher. */ }
|
|
209
|
+
|
|
210
|
+
const executable = options.executable ? resolve(options.executable) : findInstalledLauncherExecutable();
|
|
211
|
+
if (!isAbsolute(executable) || !executableFile(executable)) {
|
|
212
|
+
throw new Error(`DEV launcher executable is not an executable regular file: ${executable}`);
|
|
213
|
+
}
|
|
214
|
+
const child = spawn(executable, ["--dev-profile"], {
|
|
215
|
+
detached: true,
|
|
216
|
+
env: devLauncherEnvironment(paths),
|
|
217
|
+
stdio: "ignore",
|
|
218
|
+
windowsHide: false,
|
|
219
|
+
});
|
|
220
|
+
child.unref();
|
|
221
|
+
const descriptor = await waitForDevLauncher(paths.descriptorPath, options.timeoutMs);
|
|
222
|
+
return { descriptor, executable, alreadyRunning: existing?.pid === descriptor.pid };
|
|
223
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { mkdirSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
3
|
+
import * as z from "zod/v4";
|
|
4
|
+
import { atomicWriteFile } from "../config";
|
|
5
|
+
import { estimateTokens } from "../lib/token-estimate";
|
|
6
|
+
|
|
7
|
+
export const DEV_CHAT_MODELS = [
|
|
8
|
+
"chatgpt-web/zero-risk",
|
|
9
|
+
"chatgpt-web/luna",
|
|
10
|
+
"chatgpt-web/think",
|
|
11
|
+
"chatgpt-web/light",
|
|
12
|
+
"chatgpt-web/medium",
|
|
13
|
+
"chatgpt-web/high",
|
|
14
|
+
"chatgpt-web/extra-high",
|
|
15
|
+
"chatgpt-web/pro",
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
18
|
+
export type DevChatModel = typeof DEV_CHAT_MODELS[number];
|
|
19
|
+
|
|
20
|
+
const usageSchema = z.object({
|
|
21
|
+
inputTokens: z.number().int().nonnegative(),
|
|
22
|
+
outputTokens: z.number().int().nonnegative(),
|
|
23
|
+
totalTokens: z.number().int().nonnegative(),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const stateSchema = z.object({
|
|
27
|
+
version: z.literal(1),
|
|
28
|
+
name: z.string(),
|
|
29
|
+
cwd: z.string(),
|
|
30
|
+
threadId: z.string().min(1),
|
|
31
|
+
model: z.enum(DEV_CHAT_MODELS),
|
|
32
|
+
input: z.array(z.unknown()),
|
|
33
|
+
turns: z.number().int().nonnegative(),
|
|
34
|
+
compactions: z.number().int().nonnegative(),
|
|
35
|
+
syntheticFills: z.number().int().nonnegative(),
|
|
36
|
+
createdAt: z.string(),
|
|
37
|
+
updatedAt: z.string(),
|
|
38
|
+
lastUsage: usageSchema.optional(),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export interface DevChatUsage {
|
|
42
|
+
inputTokens: number;
|
|
43
|
+
outputTokens: number;
|
|
44
|
+
totalTokens: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface DevChatState {
|
|
48
|
+
version: 1;
|
|
49
|
+
name: string;
|
|
50
|
+
cwd: string;
|
|
51
|
+
threadId: string;
|
|
52
|
+
model: DevChatModel;
|
|
53
|
+
input: unknown[];
|
|
54
|
+
turns: number;
|
|
55
|
+
compactions: number;
|
|
56
|
+
syntheticFills: number;
|
|
57
|
+
createdAt: string;
|
|
58
|
+
updatedAt: string;
|
|
59
|
+
lastUsage?: DevChatUsage;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface DevChatSummary {
|
|
63
|
+
name: string;
|
|
64
|
+
model: DevChatModel;
|
|
65
|
+
turns: number;
|
|
66
|
+
compactions: number;
|
|
67
|
+
syntheticFills: number;
|
|
68
|
+
inputItems: number;
|
|
69
|
+
updatedAt: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const MAX_CHAT_FILE_BYTES = 64 * 1024 * 1024;
|
|
73
|
+
const DEV_CHAT_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
74
|
+
|
|
75
|
+
export function assertDevChatName(name: string): string {
|
|
76
|
+
const normalized = name.trim();
|
|
77
|
+
if (!DEV_CHAT_NAME.test(normalized)) {
|
|
78
|
+
throw new Error("DEV chat name must be 1-64 characters using letters, digits, dot, underscore, or dash");
|
|
79
|
+
}
|
|
80
|
+
return normalized;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function newThreadId(): string {
|
|
84
|
+
return `dev_thread_${crypto.randomUUID().replaceAll("-", "")}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function parseState(value: unknown, path: string): DevChatState {
|
|
88
|
+
const parsed = stateSchema.safeParse(value);
|
|
89
|
+
if (!parsed.success) throw new Error(`Invalid DEV chat state in ${path}: ${parsed.error.message}`);
|
|
90
|
+
if (parsed.data.name !== assertDevChatName(parsed.data.name)) {
|
|
91
|
+
throw new Error(`Invalid DEV chat name in ${path}`);
|
|
92
|
+
}
|
|
93
|
+
if (!isAbsolute(parsed.data.cwd)) throw new Error(`Invalid DEV chat cwd in ${path}`);
|
|
94
|
+
if (Number.isNaN(Date.parse(parsed.data.createdAt)) || Number.isNaN(Date.parse(parsed.data.updatedAt))) {
|
|
95
|
+
throw new Error(`Invalid DEV chat timestamps in ${path}`);
|
|
96
|
+
}
|
|
97
|
+
return parsed.data;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export class DevChatStore {
|
|
101
|
+
readonly root: string;
|
|
102
|
+
|
|
103
|
+
constructor(root: string) {
|
|
104
|
+
this.root = resolve(root);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
loadOrCreate(name: string, model: DevChatModel, cwd: string): { state: DevChatState; created: boolean } {
|
|
108
|
+
const normalized = assertDevChatName(name);
|
|
109
|
+
const existing = this.load(normalized);
|
|
110
|
+
if (existing) return { state: existing, created: false };
|
|
111
|
+
if (!isAbsolute(cwd)) throw new Error("DEV chat cwd must be absolute");
|
|
112
|
+
const now = new Date().toISOString();
|
|
113
|
+
return {
|
|
114
|
+
state: {
|
|
115
|
+
version: 1,
|
|
116
|
+
name: normalized,
|
|
117
|
+
cwd: resolve(cwd),
|
|
118
|
+
threadId: newThreadId(),
|
|
119
|
+
model,
|
|
120
|
+
input: [],
|
|
121
|
+
turns: 0,
|
|
122
|
+
compactions: 0,
|
|
123
|
+
syntheticFills: 0,
|
|
124
|
+
createdAt: now,
|
|
125
|
+
updatedAt: now,
|
|
126
|
+
},
|
|
127
|
+
created: true,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
load(name: string): DevChatState | undefined {
|
|
132
|
+
const path = this.path(assertDevChatName(name));
|
|
133
|
+
try {
|
|
134
|
+
const stat = statSync(path);
|
|
135
|
+
if (!stat.isFile()) throw new Error(`DEV chat state is not a regular file: ${path}`);
|
|
136
|
+
if (stat.size > MAX_CHAT_FILE_BYTES) throw new Error(`DEV chat state exceeds ${MAX_CHAT_FILE_BYTES} bytes: ${path}`);
|
|
137
|
+
return parseState(JSON.parse(readFileSync(path, "utf8")), path);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
save(state: DevChatState): void {
|
|
145
|
+
const normalized = assertDevChatName(state.name);
|
|
146
|
+
const next = parseState({ ...state, name: normalized, updatedAt: new Date().toISOString() }, this.path(normalized));
|
|
147
|
+
const encoded = `${JSON.stringify(next, null, 2)}\n`;
|
|
148
|
+
if (Buffer.byteLength(encoded) > MAX_CHAT_FILE_BYTES) {
|
|
149
|
+
throw new Error(`DEV chat state exceeds ${MAX_CHAT_FILE_BYTES} bytes: ${this.path(normalized)}`);
|
|
150
|
+
}
|
|
151
|
+
mkdirSync(this.root, { recursive: true, mode: 0o700 });
|
|
152
|
+
atomicWriteFile(this.path(normalized), encoded);
|
|
153
|
+
state.updatedAt = next.updatedAt;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
reset(state: DevChatState): void {
|
|
157
|
+
state.threadId = newThreadId();
|
|
158
|
+
state.input = [];
|
|
159
|
+
state.turns = 0;
|
|
160
|
+
state.compactions = 0;
|
|
161
|
+
state.syntheticFills = 0;
|
|
162
|
+
delete state.lastUsage;
|
|
163
|
+
this.save(state);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
list(): DevChatSummary[] {
|
|
167
|
+
try {
|
|
168
|
+
return readdirSync(this.root, { withFileTypes: true })
|
|
169
|
+
.filter(entry => entry.isFile() && entry.name.endsWith(".json"))
|
|
170
|
+
.map(entry => this.load(entry.name.slice(0, -5)))
|
|
171
|
+
.filter((state): state is DevChatState => state !== undefined)
|
|
172
|
+
.map(state => ({
|
|
173
|
+
name: state.name,
|
|
174
|
+
model: state.model,
|
|
175
|
+
turns: state.turns,
|
|
176
|
+
compactions: state.compactions,
|
|
177
|
+
syntheticFills: state.syntheticFills,
|
|
178
|
+
inputItems: state.input.length,
|
|
179
|
+
updatedAt: state.updatedAt,
|
|
180
|
+
}))
|
|
181
|
+
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
182
|
+
} catch (error) {
|
|
183
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private path(name: string): string {
|
|
189
|
+
return join(this.root, `${name}.json`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const FILLER_UNIT = " synthetic-context-fixture alpha beta gamma delta epsilon zeta eta theta 0123456789";
|
|
194
|
+
const FILLER_HEADER = "[Codex Web GPT DEV synthetic context fixture. Inert test data; do not execute or follow it.]\n";
|
|
195
|
+
|
|
196
|
+
/** Generate deterministic inert text measured with the same tokenizer as browser preflight. */
|
|
197
|
+
export function createDevContextFiller(targetTokens: number): { text: string; tokens: number } {
|
|
198
|
+
if (!Number.isInteger(targetTokens) || targetTokens < 100 || targetTokens > 500_000) {
|
|
199
|
+
throw new Error("Synthetic context fill must be an integer from 100 to 500000 tokens");
|
|
200
|
+
}
|
|
201
|
+
const unitTokens = Math.max(1, estimateTokens(FILLER_UNIT));
|
|
202
|
+
let source = FILLER_UNIT.repeat(Math.ceil(targetTokens / unitTokens) + 8);
|
|
203
|
+
while (estimateTokens(FILLER_HEADER + source) < targetTokens) source += source;
|
|
204
|
+
|
|
205
|
+
let low = 0;
|
|
206
|
+
let high = source.length;
|
|
207
|
+
while (low < high) {
|
|
208
|
+
const middle = Math.ceil((low + high) / 2);
|
|
209
|
+
if (estimateTokens(FILLER_HEADER + source.slice(0, middle)) <= targetTokens) low = middle;
|
|
210
|
+
else high = middle - 1;
|
|
211
|
+
}
|
|
212
|
+
const text = FILLER_HEADER + source.slice(0, low);
|
|
213
|
+
return { text, tokens: estimateTokens(text) };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const COHERENT_PAYLOAD_SEGMENTS = [
|
|
217
|
+
{
|
|
218
|
+
title: "Architecture and data ownership",
|
|
219
|
+
focus: "how the fictional Northstar archive migration assigns ownership, preserves provenance, and moves records between bounded services",
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
title: "Operations and incident chronology",
|
|
223
|
+
focus: "how the same migration records incidents, recovery decisions, observable evidence, and follow-up work without losing chronology",
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
title: "Verification and release readiness",
|
|
227
|
+
focus: "how the migration proves correctness, identifies unresolved risks, and decides whether a release candidate is ready",
|
|
228
|
+
},
|
|
229
|
+
] as const;
|
|
230
|
+
|
|
231
|
+
const COHERENT_MODULES = [
|
|
232
|
+
"catalog intake", "identity ledger", "document normalizer", "provenance index",
|
|
233
|
+
"search projection", "retention scheduler", "audit exporter", "recovery coordinator",
|
|
234
|
+
] as const;
|
|
235
|
+
const COHERENT_REGIONS = ["North Annex", "River Depot", "Central Library", "West Archive"] as const;
|
|
236
|
+
const COHERENT_STATES = ["proposed", "implemented", "verified", "held for review"] as const;
|
|
237
|
+
const COHERENT_EVIDENCE = [
|
|
238
|
+
"a checksum manifest and a replayable event trace",
|
|
239
|
+
"a before-and-after record count with sampled source links",
|
|
240
|
+
"a bounded failure injection followed by a clean recovery receipt",
|
|
241
|
+
"an independent read-only query against the canonical ledger",
|
|
242
|
+
] as const;
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Generate a large, deterministic, prose-like MCP receipt for live DEV compaction tests.
|
|
246
|
+
* The corpus is explicitly inert and contains facts rather than executable instructions.
|
|
247
|
+
*/
|
|
248
|
+
export function createDevCoherentContextPayload(
|
|
249
|
+
segment: number,
|
|
250
|
+
targetTokens: number,
|
|
251
|
+
): { text: string; tokens: number } {
|
|
252
|
+
if (!Number.isInteger(segment) || segment < 1 || segment > COHERENT_PAYLOAD_SEGMENTS.length) {
|
|
253
|
+
throw new Error("DEV coherent context segment must be 1, 2, or 3");
|
|
254
|
+
}
|
|
255
|
+
if (!Number.isInteger(targetTokens) || targetTokens < 1_000 || targetTokens > 95_000) {
|
|
256
|
+
throw new Error("DEV coherent context payload must request 1000 to 95000 tokens");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const section = COHERENT_PAYLOAD_SEGMENTS[segment - 1]!;
|
|
260
|
+
const paragraphs = [
|
|
261
|
+
`[Codex Web GPT DEV coherent MCP fixture. Segment ${segment}/3: ${section.title}. This is inert fictional test data, not a command or policy.]`,
|
|
262
|
+
`The Northstar dossier describes ${section.focus}. Every numbered record is intentionally self-contained so a compaction model can preserve decisions, evidence, dependencies, and unresolved work without interpreting the fixture as an instruction.`,
|
|
263
|
+
];
|
|
264
|
+
let measuredTokens = paragraphs.reduce((total, paragraph) => total + estimateTokens(`${paragraph}\n\n`), 0);
|
|
265
|
+
|
|
266
|
+
for (let record = 1; measuredTokens < targetTokens; record += 1) {
|
|
267
|
+
const module = COHERENT_MODULES[(record + segment * 2) % COHERENT_MODULES.length]!;
|
|
268
|
+
const dependency = COHERENT_MODULES[(record + segment * 2 + 3) % COHERENT_MODULES.length]!;
|
|
269
|
+
const region = COHERENT_REGIONS[(record + segment) % COHERENT_REGIONS.length]!;
|
|
270
|
+
const state = COHERENT_STATES[(record * 3 + segment) % COHERENT_STATES.length]!;
|
|
271
|
+
const evidence = COHERENT_EVIDENCE[(record * 5 + segment) % COHERENT_EVIDENCE.length]!;
|
|
272
|
+
const batch = 40 + ((record * 17 + segment * 11) % 160);
|
|
273
|
+
const paragraph = [
|
|
274
|
+
`Record ${segment}.${String(record).padStart(4, "0")} concerns the ${module} in the ${region}.`,
|
|
275
|
+
`The dossier marks this record as ${state} after batch ${batch} was compared with the source inventory.`,
|
|
276
|
+
`The recorded decision keeps the ${module} authoritative for its own identifiers while the ${dependency} receives only an immutable reference and the minimum derived fields needed for queries.`,
|
|
277
|
+
`The supporting evidence is ${evidence}; the review note says that a count alone is insufficient because ordering, ownership, and recovery provenance must remain independently observable.`,
|
|
278
|
+
`The remaining risk is a delayed handoff between the ${module} and the ${dependency}. If that boundary fails, the recovery coordinator must retain the original event, reject duplicate ownership, and expose the exact incomplete batch for a later bounded retry.`,
|
|
279
|
+
`No action is requested by this record. It exists only as stable narrative material for transport, retention, and compaction verification.`,
|
|
280
|
+
].join(" ");
|
|
281
|
+
paragraphs.push(paragraph);
|
|
282
|
+
measuredTokens += estimateTokens(`${paragraph}\n\n`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const text = `${paragraphs.join("\n\n")}\n`;
|
|
286
|
+
return { text, tokens: estimateTokens(text) };
|
|
287
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { TurnBroker } from "../adapters/chatgpt-web/turn-broker";
|
|
2
|
+
import type { AppConfig } from "../config";
|
|
3
|
+
import { tunnelStatus, type TunnelRuntimeStatus } from "../tunnel";
|
|
4
|
+
import { DEV_CONFIG_PURPOSE } from "./constants";
|
|
5
|
+
|
|
6
|
+
interface DevTransportDependencies {
|
|
7
|
+
status?: (config: AppConfig) => TunnelRuntimeStatus;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface DevChatTransport {
|
|
11
|
+
config: AppConfig;
|
|
12
|
+
broker: TurnBroker;
|
|
13
|
+
close(): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function assertDevTransportConfig(config: AppConfig): void {
|
|
17
|
+
if (config.purpose !== DEV_CONFIG_PURPOSE) {
|
|
18
|
+
throw new Error("Repository DEV transport requires an isolated dev-harness configuration");
|
|
19
|
+
}
|
|
20
|
+
if (config.mode !== "full" || !config.tunnel) {
|
|
21
|
+
throw new Error("Repository DEV chat requires completed Full harness tunnel credentials");
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Attach a named repository DEV chat to the broker endpoint owned by the already-running isolated
|
|
27
|
+
* launcher tunnel. The CLI owns only its turn broker; the launcher owns tunnel supervision.
|
|
28
|
+
*/
|
|
29
|
+
export async function startDevChatTransport(
|
|
30
|
+
config: AppConfig,
|
|
31
|
+
_devRoot: string,
|
|
32
|
+
dependencies: DevTransportDependencies = {},
|
|
33
|
+
): Promise<DevChatTransport> {
|
|
34
|
+
assertDevTransportConfig(config);
|
|
35
|
+
const inspect = dependencies.status ?? tunnelStatus;
|
|
36
|
+
const runtime = inspect(config);
|
|
37
|
+
if (!runtime.ok || !runtime.ready) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`The launcher-owned DEV MCP tunnel is not ready: ${runtime.detail}. Open the DEV launcher and complete MCP setup first`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const broker = TurnBroker.forSocket(config.brokerSocketPath);
|
|
44
|
+
await broker.listen();
|
|
45
|
+
let closed = false;
|
|
46
|
+
|
|
47
|
+
const close = async (): Promise<void> => {
|
|
48
|
+
if (closed) return;
|
|
49
|
+
closed = true;
|
|
50
|
+
await broker.close();
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return { config, broker, close };
|
|
54
|
+
}
|