codex-sidecar 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.
- package/LICENSE +21 -0
- package/README.md +86 -0
- package/dist/codex/app-server-client.d.ts +2 -0
- package/dist/codex/app-server-client.js +299 -0
- package/dist/codex/defaults.d.ts +2 -0
- package/dist/codex/defaults.js +2 -0
- package/dist/codex/session.d.ts +1 -0
- package/dist/codex/session.js +1 -0
- package/dist/codex/state.d.ts +10 -0
- package/dist/codex/state.js +68 -0
- package/dist/codex/types.d.ts +34 -0
- package/dist/codex/types.js +1 -0
- package/dist/commands/ask.d.ts +2 -0
- package/dist/commands/ask.js +37 -0
- package/dist/commands/errors.d.ts +7 -0
- package/dist/commands/errors.js +32 -0
- package/dist/commands/reset.d.ts +2 -0
- package/dist/commands/reset.js +54 -0
- package/dist/commands/start.d.ts +2 -0
- package/dist/commands/start.js +29 -0
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +43 -0
- package/dist/commands/stop.d.ts +2 -0
- package/dist/commands/stop.js +41 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/main.d.ts +7 -0
- package/dist/main.js +61 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 nora
|
|
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,86 @@
|
|
|
1
|
+
# codex-sidecar
|
|
2
|
+
|
|
3
|
+
Claude Code から Codex App Server を薄い CLI 経由で呼び出し、同じ Codex thread を継続利用するための小さな sidecar。
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g codex-sidecar
|
|
9
|
+
npx skills add https://github.com/nora/codex-sidecar --yes --global
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
前提:
|
|
13
|
+
|
|
14
|
+
- Node.js 22+
|
|
15
|
+
- `codex` CLI が使えること
|
|
16
|
+
- `codex app-server --listen stdio://` が動くこと
|
|
17
|
+
|
|
18
|
+
## Why
|
|
19
|
+
|
|
20
|
+
- Claude Code を主担当にする
|
|
21
|
+
- Codex を senior engineer / reviewer として横に置く
|
|
22
|
+
- broker や Claude Channel を入れず、小さい構成で長いラリーを回せるようにする
|
|
23
|
+
|
|
24
|
+
## Current Scope
|
|
25
|
+
|
|
26
|
+
- `codex app-server` を各 CLI 実行時にローカル子プロセスで起動する
|
|
27
|
+
- 1 本の `thread` を state file で保持し、次回以降は resume する
|
|
28
|
+
- CLI から `start / ask / status / reset / stop` を提供する
|
|
29
|
+
- Claude Code 側は Bash または plugin からこの CLI を叩く
|
|
30
|
+
- 既定モデルは `gpt-5.4`、既定 reasoning effort は `high`
|
|
31
|
+
- 現状は state file が `.agents/state/codex-sidecar.json` 固定なので、`1 cwd = 1 sidecar session`
|
|
32
|
+
- thread state は各プロジェクト配下の `.agents/state/codex-sidecar.json` に保存する
|
|
33
|
+
|
|
34
|
+
`stdio://` は別プロセスから再接続できないため、現実装では常駐 app-server は持たず、各 command が app-server を起動して `threadId` / `threadPath` を再利用する。
|
|
35
|
+
`start` は resume 可能な rollout を materialize するために bootstrap turn を 1 回だけ流す。
|
|
36
|
+
|
|
37
|
+
## Commands
|
|
38
|
+
|
|
39
|
+
グローバルインストール後:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
codex-sidecar start
|
|
43
|
+
codex-sidecar ask "この設計の弱点を挙げて"
|
|
44
|
+
codex-sidecar ask "さっきの2番目の弱点について、最小修正案を具体化して"
|
|
45
|
+
codex-sidecar status
|
|
46
|
+
codex-sidecar reset
|
|
47
|
+
codex-sidecar ask "新しい前提で、この実装方針をレビューして"
|
|
48
|
+
codex-sidecar stop
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
リポジトリ内での開発:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pnpm install
|
|
55
|
+
pnpm qc
|
|
56
|
+
pnpm dev -- help
|
|
57
|
+
pnpm dev -- start
|
|
58
|
+
pnpm dev -- ask "この設計の弱点を挙げて"
|
|
59
|
+
pnpm dev -- status
|
|
60
|
+
pnpm dev -- reset
|
|
61
|
+
pnpm dev -- stop
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Recovery
|
|
65
|
+
|
|
66
|
+
- `codex-sidecar status` で現在の thread / state を確認する
|
|
67
|
+
- `ask` が resume/state エラーなら `codex-sidecar reset`
|
|
68
|
+
- state file を強制的に消したいなら `codex-sidecar stop`
|
|
69
|
+
|
|
70
|
+
## Quality
|
|
71
|
+
|
|
72
|
+
ローカル CI の入口は `pnpm qc`。
|
|
73
|
+
|
|
74
|
+
- `pnpm lint`
|
|
75
|
+
- `pnpm fmt:check`
|
|
76
|
+
- `pnpm typecheck`
|
|
77
|
+
- `pnpm test`
|
|
78
|
+
- `pnpm build`
|
|
79
|
+
|
|
80
|
+
Claude Code の `PostToolUse` hook で、`src/*.ts` への編集後に `oxlint --fix` と `oxfmt --write` が自動で走る。
|
|
81
|
+
|
|
82
|
+
## Docs
|
|
83
|
+
|
|
84
|
+
- [docs/npm.md](docs/npm.md): npm 初回公開・更新・セキュリティ手順
|
|
85
|
+
- [tasks/progress.md](tasks/progress.md): 軽量ロードマップと進捗チェックリスト
|
|
86
|
+
- [AGENTS.md](AGENTS.md): リポジトリ運用ルール
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { DEFAULT_MODEL, DEFAULT_REASONING_EFFORT } from "./defaults.js";
|
|
3
|
+
const CLIENT_VERSION = "0.0.0";
|
|
4
|
+
const TURN_TIMEOUT_MS = 5 * 60 * 1000;
|
|
5
|
+
const SIDECAR_BASE_INSTRUCTIONS = [
|
|
6
|
+
"あなたはこのリポジトリの senior engineer です。",
|
|
7
|
+
"KISS / DRY / YAGNI を優先してください。",
|
|
8
|
+
"日本語で返答してください。",
|
|
9
|
+
"批判的にレビューしてください。",
|
|
10
|
+
"必要なら代替案を提案してください。",
|
|
11
|
+
"返答は簡潔にしてください。",
|
|
12
|
+
].join("\n");
|
|
13
|
+
export async function createAppServerClient(cwd) {
|
|
14
|
+
const client = new AppServerClient(cwd);
|
|
15
|
+
await client.initialize();
|
|
16
|
+
return client;
|
|
17
|
+
}
|
|
18
|
+
class AppServerClient {
|
|
19
|
+
child;
|
|
20
|
+
pendingRequests = new Map();
|
|
21
|
+
agentMessages = new Map();
|
|
22
|
+
completedTurns = new Map();
|
|
23
|
+
turnWaiters = new Map();
|
|
24
|
+
stderrLines = [];
|
|
25
|
+
cwd;
|
|
26
|
+
nextId = 1;
|
|
27
|
+
stdoutBuffer = "";
|
|
28
|
+
closed = false;
|
|
29
|
+
constructor(cwd) {
|
|
30
|
+
this.cwd = cwd;
|
|
31
|
+
this.child = spawn("codex", ["app-server", "--listen", "stdio://"], {
|
|
32
|
+
cwd,
|
|
33
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
34
|
+
});
|
|
35
|
+
this.child.stdout.setEncoding("utf8");
|
|
36
|
+
this.child.stdout.on("data", (chunk) => {
|
|
37
|
+
this.handleStdoutChunk(chunk);
|
|
38
|
+
});
|
|
39
|
+
this.child.stderr.setEncoding("utf8");
|
|
40
|
+
this.child.stderr.on("data", (chunk) => {
|
|
41
|
+
this.handleStderrChunk(chunk);
|
|
42
|
+
});
|
|
43
|
+
this.child.on("exit", (code, signal) => {
|
|
44
|
+
this.handleExit(code, signal);
|
|
45
|
+
});
|
|
46
|
+
this.child.on("error", (error) => {
|
|
47
|
+
this.handleFatalError(error);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
async close() {
|
|
51
|
+
if (this.closed) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
this.closed = true;
|
|
55
|
+
this.child.stdin.end();
|
|
56
|
+
this.child.kill();
|
|
57
|
+
}
|
|
58
|
+
async createThread() {
|
|
59
|
+
const response = await this.request("thread/start", {
|
|
60
|
+
model: DEFAULT_MODEL,
|
|
61
|
+
cwd: this.cwd,
|
|
62
|
+
approvalPolicy: "never",
|
|
63
|
+
sandbox: "workspace-write",
|
|
64
|
+
baseInstructions: SIDECAR_BASE_INSTRUCTIONS,
|
|
65
|
+
personality: "pragmatic",
|
|
66
|
+
experimentalRawEvents: false,
|
|
67
|
+
persistExtendedHistory: true,
|
|
68
|
+
});
|
|
69
|
+
return response.thread;
|
|
70
|
+
}
|
|
71
|
+
async resumeThread(threadId, threadPath) {
|
|
72
|
+
const response = await this.request("thread/resume", {
|
|
73
|
+
threadId,
|
|
74
|
+
path: threadPath ?? undefined,
|
|
75
|
+
model: DEFAULT_MODEL,
|
|
76
|
+
cwd: this.cwd,
|
|
77
|
+
approvalPolicy: "never",
|
|
78
|
+
sandbox: "workspace-write",
|
|
79
|
+
baseInstructions: SIDECAR_BASE_INSTRUCTIONS,
|
|
80
|
+
personality: "pragmatic",
|
|
81
|
+
persistExtendedHistory: true,
|
|
82
|
+
});
|
|
83
|
+
return response.thread;
|
|
84
|
+
}
|
|
85
|
+
async archiveThread(threadId) {
|
|
86
|
+
await this.request("thread/archive", { threadId });
|
|
87
|
+
}
|
|
88
|
+
async startTurn(threadId, message) {
|
|
89
|
+
const response = await this.request("turn/start", {
|
|
90
|
+
threadId,
|
|
91
|
+
effort: DEFAULT_REASONING_EFFORT,
|
|
92
|
+
input: [
|
|
93
|
+
{
|
|
94
|
+
type: "text",
|
|
95
|
+
text: message,
|
|
96
|
+
text_elements: [],
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
});
|
|
100
|
+
const turnId = response.turn.id;
|
|
101
|
+
const completed = this.completedTurns.get(turnId);
|
|
102
|
+
if (completed) {
|
|
103
|
+
return this.buildTurnResult(turnId, completed);
|
|
104
|
+
}
|
|
105
|
+
return await new Promise((resolve, reject) => {
|
|
106
|
+
const timeout = setTimeout(() => {
|
|
107
|
+
this.turnWaiters.delete(turnId);
|
|
108
|
+
reject(new Error(`Timed out waiting for turn completion: ${turnId}`));
|
|
109
|
+
}, TURN_TIMEOUT_MS);
|
|
110
|
+
this.turnWaiters.set(turnId, { resolve, reject, timeout });
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
async initialize() {
|
|
114
|
+
await this.request("initialize", {
|
|
115
|
+
clientInfo: {
|
|
116
|
+
name: "codex-sidecar",
|
|
117
|
+
title: "codex-sidecar",
|
|
118
|
+
version: CLIENT_VERSION,
|
|
119
|
+
},
|
|
120
|
+
capabilities: {
|
|
121
|
+
experimentalApi: true,
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
handleStdoutChunk(chunk) {
|
|
126
|
+
this.stdoutBuffer += chunk;
|
|
127
|
+
let newlineIndex = this.stdoutBuffer.indexOf("\n");
|
|
128
|
+
while (newlineIndex >= 0) {
|
|
129
|
+
const line = this.stdoutBuffer.slice(0, newlineIndex).trim();
|
|
130
|
+
this.stdoutBuffer = this.stdoutBuffer.slice(newlineIndex + 1);
|
|
131
|
+
if (line) {
|
|
132
|
+
this.handleMessage(line);
|
|
133
|
+
}
|
|
134
|
+
newlineIndex = this.stdoutBuffer.indexOf("\n");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
handleStderrChunk(chunk) {
|
|
138
|
+
for (const line of chunk.split("\n")) {
|
|
139
|
+
const trimmed = line.trim();
|
|
140
|
+
if (!trimmed) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
this.stderrLines.push(trimmed);
|
|
144
|
+
if (this.stderrLines.length > 20) {
|
|
145
|
+
this.stderrLines.shift();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
handleMessage(line) {
|
|
150
|
+
const message = JSON.parse(line);
|
|
151
|
+
if (typeof message.method === "string") {
|
|
152
|
+
this.handleNotification(message.method, message.params);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (typeof message.id !== "number") {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const handler = this.pendingRequests.get(message.id);
|
|
159
|
+
if (!handler) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
this.pendingRequests.delete(message.id);
|
|
163
|
+
if (isJsonRpcFailure(message)) {
|
|
164
|
+
handler.reject(new Error(message.error.message));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if ("result" in message) {
|
|
168
|
+
handler.resolve(message.result);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
handler.reject(new Error("Received malformed JSON-RPC response"));
|
|
172
|
+
}
|
|
173
|
+
handleNotification(method, params) {
|
|
174
|
+
if (!isRecord(params)) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (method === "item/completed") {
|
|
178
|
+
this.handleItemCompleted(params);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (method === "turn/completed") {
|
|
182
|
+
this.handleTurnCompleted(params);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
handleItemCompleted(params) {
|
|
186
|
+
const { turnId, item } = params;
|
|
187
|
+
if (typeof turnId !== "string" || !isRecord(item)) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (item.type !== "agentMessage" || typeof item.text !== "string") {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const messages = this.agentMessages.get(turnId) ?? [];
|
|
194
|
+
messages.push(item.text);
|
|
195
|
+
this.agentMessages.set(turnId, messages);
|
|
196
|
+
}
|
|
197
|
+
handleTurnCompleted(params) {
|
|
198
|
+
const { turn } = params;
|
|
199
|
+
if (!isRecord(turn) || typeof turn.id !== "string") {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const status = normalizeTurnStatus(turn.status);
|
|
203
|
+
if (!status) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const errorMessage = isRecord(turn.error) && typeof turn.error.message === "string" ? turn.error.message : null;
|
|
207
|
+
const completedTurn = { status, errorMessage };
|
|
208
|
+
this.completedTurns.set(turn.id, completedTurn);
|
|
209
|
+
const waiter = this.turnWaiters.get(turn.id);
|
|
210
|
+
if (!waiter) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
clearTimeout(waiter.timeout);
|
|
214
|
+
this.turnWaiters.delete(turn.id);
|
|
215
|
+
waiter.resolve(this.buildTurnResult(turn.id, completedTurn));
|
|
216
|
+
}
|
|
217
|
+
buildTurnResult(turnId, completedTurn) {
|
|
218
|
+
const message = (this.agentMessages.get(turnId) ?? []).join("\n\n").trim();
|
|
219
|
+
return {
|
|
220
|
+
turnId,
|
|
221
|
+
status: completedTurn.status,
|
|
222
|
+
message,
|
|
223
|
+
errorMessage: completedTurn.errorMessage,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
handleExit(code, signal) {
|
|
227
|
+
if (this.closed) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const reason = new Error(`codex app-server exited unexpectedly (${formatExit(code, signal)})${this.formatStderrSuffix()}`);
|
|
231
|
+
this.closed = true;
|
|
232
|
+
this.rejectAll(reason);
|
|
233
|
+
}
|
|
234
|
+
handleFatalError(error) {
|
|
235
|
+
if (this.closed) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
this.closed = true;
|
|
239
|
+
this.rejectAll(error);
|
|
240
|
+
}
|
|
241
|
+
rejectAll(error) {
|
|
242
|
+
for (const handler of this.pendingRequests.values()) {
|
|
243
|
+
handler.reject(error);
|
|
244
|
+
}
|
|
245
|
+
this.pendingRequests.clear();
|
|
246
|
+
for (const waiter of this.turnWaiters.values()) {
|
|
247
|
+
clearTimeout(waiter.timeout);
|
|
248
|
+
waiter.reject(error);
|
|
249
|
+
}
|
|
250
|
+
this.turnWaiters.clear();
|
|
251
|
+
}
|
|
252
|
+
formatStderrSuffix() {
|
|
253
|
+
if (this.stderrLines.length === 0) {
|
|
254
|
+
return "";
|
|
255
|
+
}
|
|
256
|
+
return `: ${this.stderrLines.at(-1)}`;
|
|
257
|
+
}
|
|
258
|
+
async request(method, params) {
|
|
259
|
+
if (this.closed) {
|
|
260
|
+
throw new Error("codex app-server client is already closed");
|
|
261
|
+
}
|
|
262
|
+
const id = this.nextId++;
|
|
263
|
+
const payload = JSON.stringify({
|
|
264
|
+
jsonrpc: "2.0",
|
|
265
|
+
id,
|
|
266
|
+
method,
|
|
267
|
+
params,
|
|
268
|
+
});
|
|
269
|
+
const responsePromise = new Promise((resolve, reject) => {
|
|
270
|
+
this.pendingRequests.set(id, {
|
|
271
|
+
resolve: (value) => resolve(value),
|
|
272
|
+
reject,
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
this.child.stdin.write(`${payload}\n`);
|
|
276
|
+
return await responsePromise;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
function normalizeTurnStatus(status) {
|
|
280
|
+
if (status === "completed" || status === "failed" || status === "interrupted") {
|
|
281
|
+
return status;
|
|
282
|
+
}
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
function formatExit(code, signal) {
|
|
286
|
+
if (signal) {
|
|
287
|
+
return `signal ${signal}`;
|
|
288
|
+
}
|
|
289
|
+
return `code ${code ?? "unknown"}`;
|
|
290
|
+
}
|
|
291
|
+
function isRecord(value) {
|
|
292
|
+
return typeof value === "object" && value !== null;
|
|
293
|
+
}
|
|
294
|
+
function isJsonRpcFailure(value) {
|
|
295
|
+
return (typeof value.id === "number" &&
|
|
296
|
+
isRecord(value.error) &&
|
|
297
|
+
typeof value.error.code === "number" &&
|
|
298
|
+
typeof value.error.message === "string");
|
|
299
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const SESSION_BOOTSTRAP_MESSAGE = "sidecar \u30BB\u30C3\u30B7\u30E7\u30F3\u3092\u958B\u59CB\u3057\u307E\u3059\u3002\u4EE5\u5F8C\u306E\u76F8\u8AC7\u306B\u5099\u3048\u3066\u304F\u3060\u3055\u3044\u3002\u3053\u306E turn \u3067\u306F\u300C\u4E86\u89E3\u3057\u307E\u3057\u305F\u3002\u300D\u3068\u3060\u3051\u8FD4\u7B54\u3057\u3066\u304F\u3060\u3055\u3044\u3002";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const SESSION_BOOTSTRAP_MESSAGE = "sidecar セッションを開始します。以後の相談に備えてください。この turn では「了解しました。」とだけ返答してください。";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SidecarState } from "./types.js";
|
|
2
|
+
export declare class InvalidStateFileError extends Error {
|
|
3
|
+
readonly stateFilePath: string;
|
|
4
|
+
constructor(stateFilePath: string, message: string);
|
|
5
|
+
}
|
|
6
|
+
export declare function isInvalidStateFileError(error: unknown): error is InvalidStateFileError;
|
|
7
|
+
export declare function getDefaultStateFilePath(cwd: string): string;
|
|
8
|
+
export declare function readState(stateFilePath: string): Promise<SidecarState | null>;
|
|
9
|
+
export declare function writeState(stateFilePath: string, state: SidecarState): Promise<void>;
|
|
10
|
+
export declare function deleteState(stateFilePath: string): Promise<void>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export class InvalidStateFileError extends Error {
|
|
4
|
+
stateFilePath;
|
|
5
|
+
constructor(stateFilePath, message) {
|
|
6
|
+
super(`Invalid sidecar state file: ${stateFilePath}: ${message}`);
|
|
7
|
+
this.stateFilePath = stateFilePath;
|
|
8
|
+
this.name = "InvalidStateFileError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function isInvalidStateFileError(error) {
|
|
12
|
+
return error instanceof InvalidStateFileError;
|
|
13
|
+
}
|
|
14
|
+
export function getDefaultStateFilePath(cwd) {
|
|
15
|
+
return path.join(cwd, ".agents", "state", "codex-sidecar.json");
|
|
16
|
+
}
|
|
17
|
+
export async function readState(stateFilePath) {
|
|
18
|
+
try {
|
|
19
|
+
const raw = await readFile(stateFilePath, "utf8");
|
|
20
|
+
return validateState(stateFilePath, JSON.parse(raw));
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (isMissingFileError(error)) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
if (error instanceof SyntaxError) {
|
|
27
|
+
throw new InvalidStateFileError(stateFilePath, "invalid JSON");
|
|
28
|
+
}
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export async function writeState(stateFilePath, state) {
|
|
33
|
+
await mkdir(path.dirname(stateFilePath), { recursive: true });
|
|
34
|
+
await writeFile(stateFilePath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
|
35
|
+
}
|
|
36
|
+
export async function deleteState(stateFilePath) {
|
|
37
|
+
await rm(stateFilePath, { force: true });
|
|
38
|
+
}
|
|
39
|
+
function validateState(stateFilePath, value) {
|
|
40
|
+
if (!isRecord(value)) {
|
|
41
|
+
throw new InvalidStateFileError(stateFilePath, "expected object");
|
|
42
|
+
}
|
|
43
|
+
const { version, threadId, threadPath, cwd, startedAt, updatedAt } = value;
|
|
44
|
+
if (version !== 1) {
|
|
45
|
+
throw new InvalidStateFileError(stateFilePath, "unsupported version");
|
|
46
|
+
}
|
|
47
|
+
if (typeof threadId !== "string" ||
|
|
48
|
+
(threadPath !== null && typeof threadPath !== "string") ||
|
|
49
|
+
typeof cwd !== "string" ||
|
|
50
|
+
typeof startedAt !== "string" ||
|
|
51
|
+
typeof updatedAt !== "string") {
|
|
52
|
+
throw new InvalidStateFileError(stateFilePath, "missing required fields");
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
version,
|
|
56
|
+
threadId,
|
|
57
|
+
threadPath,
|
|
58
|
+
cwd,
|
|
59
|
+
startedAt,
|
|
60
|
+
updatedAt,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function isRecord(value) {
|
|
64
|
+
return typeof value === "object" && value !== null;
|
|
65
|
+
}
|
|
66
|
+
function isMissingFileError(error) {
|
|
67
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
68
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface SidecarState {
|
|
2
|
+
version: 1;
|
|
3
|
+
threadId: string;
|
|
4
|
+
threadPath: string | null;
|
|
5
|
+
cwd: string;
|
|
6
|
+
startedAt: string;
|
|
7
|
+
updatedAt: string;
|
|
8
|
+
}
|
|
9
|
+
export interface CodexThread {
|
|
10
|
+
id: string;
|
|
11
|
+
path: string | null;
|
|
12
|
+
cwd: string;
|
|
13
|
+
}
|
|
14
|
+
export interface CodexTurnResult {
|
|
15
|
+
turnId: string;
|
|
16
|
+
status: "completed" | "failed" | "interrupted";
|
|
17
|
+
message: string;
|
|
18
|
+
errorMessage: string | null;
|
|
19
|
+
}
|
|
20
|
+
export interface CodexClient {
|
|
21
|
+
createThread(): Promise<CodexThread>;
|
|
22
|
+
resumeThread(threadId: string, threadPath?: string | null): Promise<CodexThread>;
|
|
23
|
+
archiveThread(threadId: string): Promise<void>;
|
|
24
|
+
startTurn(threadId: string, message: string): Promise<CodexTurnResult>;
|
|
25
|
+
close(): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
export interface CommandContext {
|
|
28
|
+
cwd: string;
|
|
29
|
+
now: () => Date;
|
|
30
|
+
stateFilePath: string;
|
|
31
|
+
stdout: (message: string) => void;
|
|
32
|
+
stderr: (message: string) => void;
|
|
33
|
+
createClient: (cwd: string) => Promise<CodexClient>;
|
|
34
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { formatResumeFailureMessage, readStateWithRecoveryHint } from "./errors.js";
|
|
2
|
+
import { writeState } from "../codex/state.js";
|
|
3
|
+
export async function runAskCommand(context, message) {
|
|
4
|
+
if (!message.trim()) {
|
|
5
|
+
throw new Error("Usage: codex-sidecar ask <message>");
|
|
6
|
+
}
|
|
7
|
+
const state = await readStateWithRecoveryHint(context.stateFilePath);
|
|
8
|
+
if (!state) {
|
|
9
|
+
throw new Error("Sidecar session is not started. Run `codex-sidecar start`.");
|
|
10
|
+
}
|
|
11
|
+
const client = await context.createClient(context.cwd);
|
|
12
|
+
try {
|
|
13
|
+
const thread = await resumeThreadWithRecoveryHint(client, state.threadId, state.threadPath);
|
|
14
|
+
const turn = await client.startTurn(thread.id, message);
|
|
15
|
+
await writeState(context.stateFilePath, {
|
|
16
|
+
...state,
|
|
17
|
+
threadPath: thread.path,
|
|
18
|
+
cwd: thread.cwd,
|
|
19
|
+
updatedAt: context.now().toISOString(),
|
|
20
|
+
});
|
|
21
|
+
if (turn.status !== "completed") {
|
|
22
|
+
throw new Error(turn.errorMessage ?? `Turn ended with status: ${turn.status}`);
|
|
23
|
+
}
|
|
24
|
+
context.stdout(turn.message);
|
|
25
|
+
}
|
|
26
|
+
finally {
|
|
27
|
+
await client.close();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async function resumeThreadWithRecoveryHint(client, threadId, threadPath) {
|
|
31
|
+
try {
|
|
32
|
+
return await client.resumeThread(threadId, threadPath);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
throw new Error(formatResumeFailureMessage(threadId, threadPath, error));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { InvalidStateFileError, isInvalidStateFileError } from "../codex/state.js";
|
|
2
|
+
import type { SidecarState } from "../codex/types.js";
|
|
3
|
+
export declare function readStateWithRecoveryHint(stateFilePath: string): Promise<SidecarState | null>;
|
|
4
|
+
export declare function formatStateRecoveryMessage(error: InvalidStateFileError): string;
|
|
5
|
+
export declare function formatResumeFailureMessage(threadId: string, threadPath: string | null, error: unknown): string;
|
|
6
|
+
export declare function formatArchiveWarning(threadId: string, error: unknown): string;
|
|
7
|
+
export { isInvalidStateFileError };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { isInvalidStateFileError, readState } from "../codex/state.js";
|
|
2
|
+
export async function readStateWithRecoveryHint(stateFilePath) {
|
|
3
|
+
try {
|
|
4
|
+
return await readState(stateFilePath);
|
|
5
|
+
}
|
|
6
|
+
catch (error) {
|
|
7
|
+
if (isInvalidStateFileError(error)) {
|
|
8
|
+
throw new Error(formatStateRecoveryMessage(error));
|
|
9
|
+
}
|
|
10
|
+
throw error;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function formatStateRecoveryMessage(error) {
|
|
14
|
+
return [
|
|
15
|
+
error.message,
|
|
16
|
+
"Run `codex-sidecar reset` to recreate the sidecar thread, or `codex-sidecar stop` to clear local state.",
|
|
17
|
+
].join("\n");
|
|
18
|
+
}
|
|
19
|
+
export function formatResumeFailureMessage(threadId, threadPath, error) {
|
|
20
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
21
|
+
return [
|
|
22
|
+
`Failed to resume sidecar thread: ${threadId}`,
|
|
23
|
+
`Thread path: ${threadPath ?? "(none)"}`,
|
|
24
|
+
`Reason: ${reason}`,
|
|
25
|
+
"Run `codex-sidecar reset` to recreate the sidecar thread, or `codex-sidecar stop` to clear local state.",
|
|
26
|
+
].join("\n");
|
|
27
|
+
}
|
|
28
|
+
export function formatArchiveWarning(threadId, error) {
|
|
29
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
30
|
+
return `Archive warning for ${threadId}: ${message}`;
|
|
31
|
+
}
|
|
32
|
+
export { isInvalidStateFileError };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { formatArchiveWarning, formatStateRecoveryMessage } from "./errors.js";
|
|
2
|
+
import { SESSION_BOOTSTRAP_MESSAGE } from "../codex/session.js";
|
|
3
|
+
import { isInvalidStateFileError, readState, writeState } from "../codex/state.js";
|
|
4
|
+
export async function runResetCommand(context) {
|
|
5
|
+
const existingState = await readExistingState(context);
|
|
6
|
+
if (existingState.kind === "missing") {
|
|
7
|
+
throw new Error("Sidecar session is not started. Run `codex-sidecar start`.");
|
|
8
|
+
}
|
|
9
|
+
const client = await context.createClient(context.cwd);
|
|
10
|
+
try {
|
|
11
|
+
if (existingState.kind === "active") {
|
|
12
|
+
try {
|
|
13
|
+
await client.archiveThread(existingState.threadId);
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
context.stderr(formatArchiveWarning(existingState.threadId, error));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const thread = await client.createThread();
|
|
20
|
+
const turn = await client.startTurn(thread.id, SESSION_BOOTSTRAP_MESSAGE);
|
|
21
|
+
if (turn.status !== "completed") {
|
|
22
|
+
throw new Error(turn.errorMessage ?? `Turn ended with status: ${turn.status}`);
|
|
23
|
+
}
|
|
24
|
+
const timestamp = context.now().toISOString();
|
|
25
|
+
await writeState(context.stateFilePath, {
|
|
26
|
+
version: 1,
|
|
27
|
+
threadId: thread.id,
|
|
28
|
+
threadPath: thread.path,
|
|
29
|
+
cwd: thread.cwd,
|
|
30
|
+
startedAt: timestamp,
|
|
31
|
+
updatedAt: timestamp,
|
|
32
|
+
});
|
|
33
|
+
context.stdout(`Reset sidecar thread: ${thread.id}`);
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
await client.close();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async function readExistingState(context) {
|
|
40
|
+
try {
|
|
41
|
+
const state = await readState(context.stateFilePath);
|
|
42
|
+
if (!state) {
|
|
43
|
+
return { kind: "missing" };
|
|
44
|
+
}
|
|
45
|
+
return { kind: "active", threadId: state.threadId };
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (!isInvalidStateFileError(error)) {
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
context.stderr(formatStateRecoveryMessage(error));
|
|
52
|
+
return { kind: "invalid" };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { SESSION_BOOTSTRAP_MESSAGE } from "../codex/session.js";
|
|
2
|
+
import { readState, writeState } from "../codex/state.js";
|
|
3
|
+
export async function runStartCommand(context) {
|
|
4
|
+
const existingState = await readState(context.stateFilePath);
|
|
5
|
+
if (existingState) {
|
|
6
|
+
throw new Error(`Sidecar session is already active: ${existingState.threadId}`);
|
|
7
|
+
}
|
|
8
|
+
const client = await context.createClient(context.cwd);
|
|
9
|
+
try {
|
|
10
|
+
const thread = await client.createThread();
|
|
11
|
+
const turn = await client.startTurn(thread.id, SESSION_BOOTSTRAP_MESSAGE);
|
|
12
|
+
if (turn.status !== "completed") {
|
|
13
|
+
throw new Error(turn.errorMessage ?? `Turn ended with status: ${turn.status}`);
|
|
14
|
+
}
|
|
15
|
+
const timestamp = context.now().toISOString();
|
|
16
|
+
await writeState(context.stateFilePath, {
|
|
17
|
+
version: 1,
|
|
18
|
+
threadId: thread.id,
|
|
19
|
+
threadPath: thread.path,
|
|
20
|
+
cwd: thread.cwd,
|
|
21
|
+
startedAt: timestamp,
|
|
22
|
+
updatedAt: timestamp,
|
|
23
|
+
});
|
|
24
|
+
context.stdout(`Started sidecar thread: ${thread.id}`);
|
|
25
|
+
}
|
|
26
|
+
finally {
|
|
27
|
+
await client.close();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { DEFAULT_MODEL, DEFAULT_REASONING_EFFORT } from "../codex/defaults.js";
|
|
2
|
+
import { formatStateRecoveryMessage, isInvalidStateFileError } from "./errors.js";
|
|
3
|
+
import { readState } from "../codex/state.js";
|
|
4
|
+
export async function runStatusCommand(context) {
|
|
5
|
+
try {
|
|
6
|
+
const state = await readState(context.stateFilePath);
|
|
7
|
+
if (!state) {
|
|
8
|
+
context.stdout(formatStoppedStatus(context.stateFilePath));
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
context.stdout([
|
|
12
|
+
"Sidecar session: active",
|
|
13
|
+
`Thread ID: ${state.threadId}`,
|
|
14
|
+
`Thread path: ${state.threadPath ?? "(none)"}`,
|
|
15
|
+
`Thread cwd: ${state.cwd}`,
|
|
16
|
+
`Started at: ${state.startedAt}`,
|
|
17
|
+
`Updated at: ${state.updatedAt}`,
|
|
18
|
+
`State file: ${context.stateFilePath}`,
|
|
19
|
+
`Default model: ${DEFAULT_MODEL}`,
|
|
20
|
+
`Default reasoning effort: ${DEFAULT_REASONING_EFFORT}`,
|
|
21
|
+
].join("\n"));
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (!isInvalidStateFileError(error)) {
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
context.stdout([
|
|
28
|
+
"Sidecar session: invalid-state",
|
|
29
|
+
`State file: ${context.stateFilePath}`,
|
|
30
|
+
formatStateRecoveryMessage(error),
|
|
31
|
+
`Default model: ${DEFAULT_MODEL}`,
|
|
32
|
+
`Default reasoning effort: ${DEFAULT_REASONING_EFFORT}`,
|
|
33
|
+
].join("\n"));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function formatStoppedStatus(stateFilePath) {
|
|
37
|
+
return [
|
|
38
|
+
"Sidecar session: stopped",
|
|
39
|
+
`State file: ${stateFilePath}`,
|
|
40
|
+
`Default model: ${DEFAULT_MODEL}`,
|
|
41
|
+
`Default reasoning effort: ${DEFAULT_REASONING_EFFORT}`,
|
|
42
|
+
].join("\n");
|
|
43
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { formatArchiveWarning, formatStateRecoveryMessage } from "./errors.js";
|
|
2
|
+
import { deleteState, isInvalidStateFileError, readState } from "../codex/state.js";
|
|
3
|
+
export async function runStopCommand(context) {
|
|
4
|
+
const state = await readStateForStop(context);
|
|
5
|
+
if (state.kind === "missing") {
|
|
6
|
+
context.stdout("No active sidecar session.");
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
if (state.kind === "active") {
|
|
10
|
+
const client = await context.createClient(context.cwd);
|
|
11
|
+
try {
|
|
12
|
+
try {
|
|
13
|
+
await client.archiveThread(state.threadId);
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
context.stderr(formatArchiveWarning(state.threadId, error));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
finally {
|
|
20
|
+
await client.close();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
await deleteState(context.stateFilePath);
|
|
24
|
+
context.stdout("Stopped sidecar session.");
|
|
25
|
+
}
|
|
26
|
+
async function readStateForStop(context) {
|
|
27
|
+
try {
|
|
28
|
+
const state = await readState(context.stateFilePath);
|
|
29
|
+
if (!state) {
|
|
30
|
+
return { kind: "missing" };
|
|
31
|
+
}
|
|
32
|
+
return { kind: "active", threadId: state.threadId };
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
if (!isInvalidStateFileError(error)) {
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
context.stderr(formatStateRecoveryMessage(error));
|
|
39
|
+
return { kind: "invalid" };
|
|
40
|
+
}
|
|
41
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/main.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { CommandContext } from "./codex/types.js";
|
|
2
|
+
export declare function getUsageText(): string;
|
|
3
|
+
interface RunCliOptions {
|
|
4
|
+
context?: Partial<CommandContext>;
|
|
5
|
+
}
|
|
6
|
+
export declare function runCli(argv: readonly string[], options?: RunCliOptions): Promise<void>;
|
|
7
|
+
export {};
|
package/dist/main.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { runAskCommand } from "./commands/ask.js";
|
|
2
|
+
import { runResetCommand } from "./commands/reset.js";
|
|
3
|
+
import { runStartCommand } from "./commands/start.js";
|
|
4
|
+
import { runStatusCommand } from "./commands/status.js";
|
|
5
|
+
import { runStopCommand } from "./commands/stop.js";
|
|
6
|
+
import { createAppServerClient } from "./codex/app-server-client.js";
|
|
7
|
+
import { getDefaultStateFilePath } from "./codex/state.js";
|
|
8
|
+
export function getUsageText() {
|
|
9
|
+
return `Usage: codex-sidecar <command> [args]
|
|
10
|
+
|
|
11
|
+
Commands:
|
|
12
|
+
start Create and save a new Codex sidecar thread
|
|
13
|
+
ask <message> Send a message to the active Codex thread
|
|
14
|
+
status Show current sidecar state and default model settings
|
|
15
|
+
reset Archive the current thread and create a new one
|
|
16
|
+
stop Archive the current thread and clear local state`;
|
|
17
|
+
}
|
|
18
|
+
export async function runCli(argv, options = {}) {
|
|
19
|
+
const context = createCommandContext(options.context);
|
|
20
|
+
const normalizedArgv = argv[0] === "--" ? argv.slice(1) : argv;
|
|
21
|
+
const [command = "help", ...rest] = normalizedArgv;
|
|
22
|
+
try {
|
|
23
|
+
switch (command) {
|
|
24
|
+
case "start":
|
|
25
|
+
await runStartCommand(context);
|
|
26
|
+
return;
|
|
27
|
+
case "ask":
|
|
28
|
+
await runAskCommand(context, rest.join(" "));
|
|
29
|
+
return;
|
|
30
|
+
case "reset":
|
|
31
|
+
await runResetCommand(context);
|
|
32
|
+
return;
|
|
33
|
+
case "status":
|
|
34
|
+
await runStatusCommand(context);
|
|
35
|
+
return;
|
|
36
|
+
case "stop":
|
|
37
|
+
await runStopCommand(context);
|
|
38
|
+
return;
|
|
39
|
+
default:
|
|
40
|
+
context.stdout(getUsageText());
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
context.stderr(getErrorMessage(error));
|
|
45
|
+
process.exitCode = 1;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function createCommandContext(overrides) {
|
|
49
|
+
const cwd = overrides?.cwd ?? process.cwd();
|
|
50
|
+
return {
|
|
51
|
+
cwd,
|
|
52
|
+
now: overrides?.now ?? (() => new Date()),
|
|
53
|
+
stateFilePath: overrides?.stateFilePath ?? getDefaultStateFilePath(cwd),
|
|
54
|
+
stdout: overrides?.stdout ?? console.log,
|
|
55
|
+
stderr: overrides?.stderr ?? console.error,
|
|
56
|
+
createClient: overrides?.createClient ?? createAppServerClient,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function getErrorMessage(error) {
|
|
60
|
+
return error instanceof Error ? error.message : String(error);
|
|
61
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "codex-sidecar",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Persistent Codex App Server sidecar CLI for Claude Code.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/nora/codex-sidecar.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/nora/codex-sidecar#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/nora/codex-sidecar/issues"
|
|
14
|
+
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=22"
|
|
17
|
+
},
|
|
18
|
+
"packageManager": "pnpm@10.29.3",
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"!dist/**/*.test.d.ts",
|
|
22
|
+
"!dist/**/*.test.js",
|
|
23
|
+
"README.md",
|
|
24
|
+
"LICENSE"
|
|
25
|
+
],
|
|
26
|
+
"bin": {
|
|
27
|
+
"codex-sidecar": "dist/index.js"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"prepack": "pnpm build",
|
|
31
|
+
"lint": "pnpm oxlint src",
|
|
32
|
+
"lint:fix": "pnpm oxlint --fix src",
|
|
33
|
+
"fmt": "pnpm oxfmt --write src",
|
|
34
|
+
"fmt:check": "pnpm oxfmt --check src",
|
|
35
|
+
"build": "tsc -p tsconfig.json",
|
|
36
|
+
"test": "pnpm vitest run",
|
|
37
|
+
"test:cov": "pnpm vitest run --coverage",
|
|
38
|
+
"typecheck": "tsc --noEmit",
|
|
39
|
+
"qc": "scripts/quality-check.sh",
|
|
40
|
+
"dev": "tsx src/index.ts"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^25.5.0",
|
|
44
|
+
"@vitest/coverage-v8": "^4.1.2",
|
|
45
|
+
"oxfmt": "^0.42.0",
|
|
46
|
+
"oxlint": "^1",
|
|
47
|
+
"tsx": "^4.20.6",
|
|
48
|
+
"typescript": "^5.8.3",
|
|
49
|
+
"vitest": "^4.1.2"
|
|
50
|
+
}
|
|
51
|
+
}
|