@zhang6hk/dsh-codex 0.0.0-stage → 0.1.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 (37) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +18 -0
  3. package/README.md +75 -2
  4. package/README.zh.md +78 -0
  5. package/cordis.patch.yml +7 -0
  6. package/lib/args.js +118 -0
  7. package/lib/codex-path.js +31 -0
  8. package/lib/commands.js +242 -0
  9. package/lib/companion.js +177 -0
  10. package/lib/index.js +105 -0
  11. package/lib/render.js +114 -0
  12. package/lib/tool.js +103 -0
  13. package/locale/en.json +6 -0
  14. package/locale/zh.json +6 -0
  15. package/package.json +49 -4
  16. package/vendor/codex/.claude-plugin/plugin.json +8 -0
  17. package/vendor/codex/prompts/adversarial-review.md +84 -0
  18. package/vendor/codex/prompts/stop-review-gate.md +36 -0
  19. package/vendor/codex/schemas/review-output.schema.json +87 -0
  20. package/vendor/codex/scripts/app-server-broker.mjs +252 -0
  21. package/vendor/codex/scripts/codex-companion.mjs +1073 -0
  22. package/vendor/codex/scripts/lib/app-server-protocol.d.ts +75 -0
  23. package/vendor/codex/scripts/lib/app-server.mjs +354 -0
  24. package/vendor/codex/scripts/lib/args.mjs +128 -0
  25. package/vendor/codex/scripts/lib/broker-endpoint.mjs +41 -0
  26. package/vendor/codex/scripts/lib/broker-lifecycle.mjs +209 -0
  27. package/vendor/codex/scripts/lib/claude-session-transfer.mjs +44 -0
  28. package/vendor/codex/scripts/lib/codex.mjs +1219 -0
  29. package/vendor/codex/scripts/lib/fs.mjs +40 -0
  30. package/vendor/codex/scripts/lib/git.mjs +347 -0
  31. package/vendor/codex/scripts/lib/job-control.mjs +308 -0
  32. package/vendor/codex/scripts/lib/process.mjs +135 -0
  33. package/vendor/codex/scripts/lib/prompts.mjs +13 -0
  34. package/vendor/codex/scripts/lib/render.mjs +465 -0
  35. package/vendor/codex/scripts/lib/state.mjs +191 -0
  36. package/vendor/codex/scripts/lib/tracked-jobs.mjs +204 -0
  37. package/vendor/codex/scripts/lib/workspace.mjs +9 -0
@@ -0,0 +1,191 @@
1
+ import { createHash } from "node:crypto";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+
6
+ import { resolveWorkspaceRoot } from "./workspace.mjs";
7
+
8
+ const STATE_VERSION = 1;
9
+ const PLUGIN_DATA_ENV = "CLAUDE_PLUGIN_DATA";
10
+ const FALLBACK_STATE_ROOT_DIR = path.join(os.tmpdir(), "codex-companion");
11
+ const STATE_FILE_NAME = "state.json";
12
+ const JOBS_DIR_NAME = "jobs";
13
+ const MAX_JOBS = 50;
14
+
15
+ function nowIso() {
16
+ return new Date().toISOString();
17
+ }
18
+
19
+ function defaultState() {
20
+ return {
21
+ version: STATE_VERSION,
22
+ config: {
23
+ stopReviewGate: false
24
+ },
25
+ jobs: []
26
+ };
27
+ }
28
+
29
+ export function resolveStateDir(cwd) {
30
+ const workspaceRoot = resolveWorkspaceRoot(cwd);
31
+ let canonicalWorkspaceRoot = workspaceRoot;
32
+ try {
33
+ canonicalWorkspaceRoot = fs.realpathSync.native(workspaceRoot);
34
+ } catch {
35
+ canonicalWorkspaceRoot = workspaceRoot;
36
+ }
37
+
38
+ const slugSource = path.basename(workspaceRoot) || "workspace";
39
+ const slug = slugSource.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "workspace";
40
+ const hash = createHash("sha256").update(canonicalWorkspaceRoot).digest("hex").slice(0, 16);
41
+ const pluginDataDir = process.env[PLUGIN_DATA_ENV];
42
+ const stateRoot = pluginDataDir ? path.join(pluginDataDir, "state") : FALLBACK_STATE_ROOT_DIR;
43
+ return path.join(stateRoot, `${slug}-${hash}`);
44
+ }
45
+
46
+ export function resolveStateFile(cwd) {
47
+ return path.join(resolveStateDir(cwd), STATE_FILE_NAME);
48
+ }
49
+
50
+ export function resolveJobsDir(cwd) {
51
+ return path.join(resolveStateDir(cwd), JOBS_DIR_NAME);
52
+ }
53
+
54
+ export function ensureStateDir(cwd) {
55
+ fs.mkdirSync(resolveJobsDir(cwd), { recursive: true });
56
+ }
57
+
58
+ export function loadState(cwd) {
59
+ const stateFile = resolveStateFile(cwd);
60
+ if (!fs.existsSync(stateFile)) {
61
+ return defaultState();
62
+ }
63
+
64
+ try {
65
+ const parsed = JSON.parse(fs.readFileSync(stateFile, "utf8"));
66
+ return {
67
+ ...defaultState(),
68
+ ...parsed,
69
+ config: {
70
+ ...defaultState().config,
71
+ ...(parsed.config ?? {})
72
+ },
73
+ jobs: Array.isArray(parsed.jobs) ? parsed.jobs : []
74
+ };
75
+ } catch {
76
+ return defaultState();
77
+ }
78
+ }
79
+
80
+ function pruneJobs(jobs) {
81
+ return [...jobs]
82
+ .sort((left, right) => String(right.updatedAt ?? "").localeCompare(String(left.updatedAt ?? "")))
83
+ .slice(0, MAX_JOBS);
84
+ }
85
+
86
+ function removeFileIfExists(filePath) {
87
+ if (filePath && fs.existsSync(filePath)) {
88
+ fs.unlinkSync(filePath);
89
+ }
90
+ }
91
+
92
+ export function saveState(cwd, state) {
93
+ const previousJobs = loadState(cwd).jobs;
94
+ ensureStateDir(cwd);
95
+ const nextJobs = pruneJobs(state.jobs ?? []);
96
+ const nextState = {
97
+ version: STATE_VERSION,
98
+ config: {
99
+ ...defaultState().config,
100
+ ...(state.config ?? {})
101
+ },
102
+ jobs: nextJobs
103
+ };
104
+
105
+ const retainedIds = new Set(nextJobs.map((job) => job.id));
106
+ for (const job of previousJobs) {
107
+ if (retainedIds.has(job.id)) {
108
+ continue;
109
+ }
110
+ removeJobFile(resolveJobFile(cwd, job.id));
111
+ removeFileIfExists(job.logFile);
112
+ }
113
+
114
+ fs.writeFileSync(resolveStateFile(cwd), `${JSON.stringify(nextState, null, 2)}\n`, "utf8");
115
+ return nextState;
116
+ }
117
+
118
+ export function updateState(cwd, mutate) {
119
+ const state = loadState(cwd);
120
+ mutate(state);
121
+ return saveState(cwd, state);
122
+ }
123
+
124
+ export function generateJobId(prefix = "job") {
125
+ const random = Math.random().toString(36).slice(2, 8);
126
+ return `${prefix}-${Date.now().toString(36)}-${random}`;
127
+ }
128
+
129
+ export function upsertJob(cwd, jobPatch) {
130
+ return updateState(cwd, (state) => {
131
+ const timestamp = nowIso();
132
+ const existingIndex = state.jobs.findIndex((job) => job.id === jobPatch.id);
133
+ if (existingIndex === -1) {
134
+ state.jobs.unshift({
135
+ createdAt: timestamp,
136
+ updatedAt: timestamp,
137
+ ...jobPatch
138
+ });
139
+ return;
140
+ }
141
+ state.jobs[existingIndex] = {
142
+ ...state.jobs[existingIndex],
143
+ ...jobPatch,
144
+ updatedAt: timestamp
145
+ };
146
+ });
147
+ }
148
+
149
+ export function listJobs(cwd) {
150
+ return loadState(cwd).jobs;
151
+ }
152
+
153
+ export function setConfig(cwd, key, value) {
154
+ return updateState(cwd, (state) => {
155
+ state.config = {
156
+ ...state.config,
157
+ [key]: value
158
+ };
159
+ });
160
+ }
161
+
162
+ export function getConfig(cwd) {
163
+ return loadState(cwd).config;
164
+ }
165
+
166
+ export function writeJobFile(cwd, jobId, payload) {
167
+ ensureStateDir(cwd);
168
+ const jobFile = resolveJobFile(cwd, jobId);
169
+ fs.writeFileSync(jobFile, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
170
+ return jobFile;
171
+ }
172
+
173
+ export function readJobFile(jobFile) {
174
+ return JSON.parse(fs.readFileSync(jobFile, "utf8"));
175
+ }
176
+
177
+ function removeJobFile(jobFile) {
178
+ if (fs.existsSync(jobFile)) {
179
+ fs.unlinkSync(jobFile);
180
+ }
181
+ }
182
+
183
+ export function resolveJobLogFile(cwd, jobId) {
184
+ ensureStateDir(cwd);
185
+ return path.join(resolveJobsDir(cwd), `${jobId}.log`);
186
+ }
187
+
188
+ export function resolveJobFile(cwd, jobId) {
189
+ ensureStateDir(cwd);
190
+ return path.join(resolveJobsDir(cwd), `${jobId}.json`);
191
+ }
@@ -0,0 +1,204 @@
1
+ import fs from "node:fs";
2
+ import process from "node:process";
3
+
4
+ import { readJobFile, resolveJobFile, resolveJobLogFile, upsertJob, writeJobFile } from "./state.mjs";
5
+
6
+ export const SESSION_ID_ENV = "CODEX_COMPANION_SESSION_ID";
7
+
8
+ export function nowIso() {
9
+ return new Date().toISOString();
10
+ }
11
+
12
+ function normalizeProgressEvent(value) {
13
+ if (value && typeof value === "object" && !Array.isArray(value)) {
14
+ return {
15
+ message: String(value.message ?? "").trim(),
16
+ phase: typeof value.phase === "string" && value.phase.trim() ? value.phase.trim() : null,
17
+ threadId: typeof value.threadId === "string" && value.threadId.trim() ? value.threadId.trim() : null,
18
+ turnId: typeof value.turnId === "string" && value.turnId.trim() ? value.turnId.trim() : null,
19
+ stderrMessage: value.stderrMessage == null ? null : String(value.stderrMessage).trim(),
20
+ logTitle: typeof value.logTitle === "string" && value.logTitle.trim() ? value.logTitle.trim() : null,
21
+ logBody: value.logBody == null ? null : String(value.logBody).trimEnd()
22
+ };
23
+ }
24
+
25
+ return {
26
+ message: String(value ?? "").trim(),
27
+ phase: null,
28
+ threadId: null,
29
+ turnId: null,
30
+ stderrMessage: String(value ?? "").trim(),
31
+ logTitle: null,
32
+ logBody: null
33
+ };
34
+ }
35
+
36
+ export function appendLogLine(logFile, message) {
37
+ const normalized = String(message ?? "").trim();
38
+ if (!logFile || !normalized) {
39
+ return;
40
+ }
41
+ fs.appendFileSync(logFile, `[${nowIso()}] ${normalized}\n`, "utf8");
42
+ }
43
+
44
+ export function appendLogBlock(logFile, title, body) {
45
+ if (!logFile || !body) {
46
+ return;
47
+ }
48
+ fs.appendFileSync(logFile, `\n[${nowIso()}] ${title}\n${String(body).trimEnd()}\n`, "utf8");
49
+ }
50
+
51
+ export function createJobLogFile(workspaceRoot, jobId, title) {
52
+ const logFile = resolveJobLogFile(workspaceRoot, jobId);
53
+ fs.writeFileSync(logFile, "", "utf8");
54
+ if (title) {
55
+ appendLogLine(logFile, `Starting ${title}.`);
56
+ }
57
+ return logFile;
58
+ }
59
+
60
+ export function createJobRecord(base, options = {}) {
61
+ const env = options.env ?? process.env;
62
+ const sessionId = env[options.sessionIdEnv ?? SESSION_ID_ENV];
63
+ return {
64
+ ...base,
65
+ createdAt: nowIso(),
66
+ ...(sessionId ? { sessionId } : {})
67
+ };
68
+ }
69
+
70
+ export function createJobProgressUpdater(workspaceRoot, jobId) {
71
+ let lastPhase = null;
72
+ let lastThreadId = null;
73
+ let lastTurnId = null;
74
+
75
+ return (event) => {
76
+ const normalized = normalizeProgressEvent(event);
77
+ const patch = { id: jobId };
78
+ let changed = false;
79
+
80
+ if (normalized.phase && normalized.phase !== lastPhase) {
81
+ lastPhase = normalized.phase;
82
+ patch.phase = normalized.phase;
83
+ changed = true;
84
+ }
85
+
86
+ if (normalized.threadId && normalized.threadId !== lastThreadId) {
87
+ lastThreadId = normalized.threadId;
88
+ patch.threadId = normalized.threadId;
89
+ changed = true;
90
+ }
91
+
92
+ if (normalized.turnId && normalized.turnId !== lastTurnId) {
93
+ lastTurnId = normalized.turnId;
94
+ patch.turnId = normalized.turnId;
95
+ changed = true;
96
+ }
97
+
98
+ if (!changed) {
99
+ return;
100
+ }
101
+
102
+ upsertJob(workspaceRoot, patch);
103
+
104
+ const jobFile = resolveJobFile(workspaceRoot, jobId);
105
+ if (!fs.existsSync(jobFile)) {
106
+ return;
107
+ }
108
+
109
+ const storedJob = readJobFile(jobFile);
110
+ writeJobFile(workspaceRoot, jobId, {
111
+ ...storedJob,
112
+ ...patch
113
+ });
114
+ };
115
+ }
116
+
117
+ export function createProgressReporter({ stderr = false, logFile = null, onEvent = null } = {}) {
118
+ if (!stderr && !logFile && !onEvent) {
119
+ return null;
120
+ }
121
+
122
+ return (eventOrMessage) => {
123
+ const event = normalizeProgressEvent(eventOrMessage);
124
+ const stderrMessage = event.stderrMessage ?? event.message;
125
+ if (stderr && stderrMessage) {
126
+ process.stderr.write(`[codex] ${stderrMessage}\n`);
127
+ }
128
+ appendLogLine(logFile, event.message);
129
+ appendLogBlock(logFile, event.logTitle, event.logBody);
130
+ onEvent?.(event);
131
+ };
132
+ }
133
+
134
+ function readStoredJobOrNull(workspaceRoot, jobId) {
135
+ const jobFile = resolveJobFile(workspaceRoot, jobId);
136
+ if (!fs.existsSync(jobFile)) {
137
+ return null;
138
+ }
139
+ return readJobFile(jobFile);
140
+ }
141
+
142
+ export async function runTrackedJob(job, runner, options = {}) {
143
+ const runningRecord = {
144
+ ...job,
145
+ status: "running",
146
+ startedAt: nowIso(),
147
+ phase: "starting",
148
+ pid: process.pid,
149
+ logFile: options.logFile ?? job.logFile ?? null
150
+ };
151
+ writeJobFile(job.workspaceRoot, job.id, runningRecord);
152
+ upsertJob(job.workspaceRoot, runningRecord);
153
+
154
+ try {
155
+ const execution = await runner();
156
+ const completionStatus = execution.exitStatus === 0 ? "completed" : "failed";
157
+ const completedAt = nowIso();
158
+ writeJobFile(job.workspaceRoot, job.id, {
159
+ ...runningRecord,
160
+ status: completionStatus,
161
+ threadId: execution.threadId ?? null,
162
+ turnId: execution.turnId ?? null,
163
+ pid: null,
164
+ phase: completionStatus === "completed" ? "done" : "failed",
165
+ completedAt,
166
+ result: execution.payload,
167
+ rendered: execution.rendered
168
+ });
169
+ upsertJob(job.workspaceRoot, {
170
+ id: job.id,
171
+ status: completionStatus,
172
+ threadId: execution.threadId ?? null,
173
+ turnId: execution.turnId ?? null,
174
+ summary: execution.summary,
175
+ phase: completionStatus === "completed" ? "done" : "failed",
176
+ pid: null,
177
+ completedAt
178
+ });
179
+ appendLogBlock(options.logFile ?? job.logFile ?? null, "Final output", execution.rendered);
180
+ return execution;
181
+ } catch (error) {
182
+ const errorMessage = error instanceof Error ? error.message : String(error);
183
+ const existing = readStoredJobOrNull(job.workspaceRoot, job.id) ?? runningRecord;
184
+ const completedAt = nowIso();
185
+ writeJobFile(job.workspaceRoot, job.id, {
186
+ ...existing,
187
+ status: "failed",
188
+ phase: "failed",
189
+ errorMessage,
190
+ pid: null,
191
+ completedAt,
192
+ logFile: options.logFile ?? job.logFile ?? existing.logFile ?? null
193
+ });
194
+ upsertJob(job.workspaceRoot, {
195
+ id: job.id,
196
+ status: "failed",
197
+ phase: "failed",
198
+ pid: null,
199
+ errorMessage,
200
+ completedAt
201
+ });
202
+ throw error;
203
+ }
204
+ }
@@ -0,0 +1,9 @@
1
+ import { ensureGitRepository } from "./git.mjs";
2
+
3
+ export function resolveWorkspaceRoot(cwd) {
4
+ try {
5
+ return ensureGitRepository(cwd);
6
+ } catch {
7
+ return cwd;
8
+ }
9
+ }