codex-relay 1.0.0 → 1.0.1-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -24
- package/dist/cli.js +7 -7
- package/dist/src.js +4 -2
- package/dist/src2.js +212 -45
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ Codex Relay runs a local bridge server for the Codex Relay mobile app. Keep Code
|
|
|
13
13
|
Run the server from the workspace you want Codex to use:
|
|
14
14
|
|
|
15
15
|
```sh
|
|
16
|
-
npx codex-relay
|
|
16
|
+
npx codex-relay@latest
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
The CLI prints a QR code, a mobile URL, and a `codex-relay://pair...` pairing payload. Scan the QR code from the mobile app. If scanning is not available, paste the full pairing payload into the app.
|
|
@@ -21,7 +21,7 @@ The CLI prints a QR code, a mobile URL, and a `codex-relay://pair...` pairing pa
|
|
|
21
21
|
When the app shows an approval code, approve it on the computer:
|
|
22
22
|
|
|
23
23
|
```sh
|
|
24
|
-
npx codex-relay approve XXXX-XXXX
|
|
24
|
+
npx codex-relay@latest approve XXXX-XXXX
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
After approval, the phone can list Codex threads, start new work, stream messages, and handle approval prompts from the local Codex runtime.
|
|
@@ -31,7 +31,7 @@ After approval, the phone can list Codex threads, start new work, stream message
|
|
|
31
31
|
To keep the relay running after the command returns:
|
|
32
32
|
|
|
33
33
|
```sh
|
|
34
|
-
npx codex-relay --bg
|
|
34
|
+
npx codex-relay@latest --bg
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
Background mode writes runtime files under `.codex-relay/` in the current directory:
|
|
@@ -44,7 +44,7 @@ Background mode writes runtime files under `.codex-relay/` in the current direct
|
|
|
44
44
|
Print the current pairing QR again:
|
|
45
45
|
|
|
46
46
|
```sh
|
|
47
|
-
npx codex-relay qr
|
|
47
|
+
npx codex-relay@latest qr
|
|
48
48
|
```
|
|
49
49
|
|
|
50
50
|
Stop a background server with the printed process id:
|
|
@@ -56,25 +56,25 @@ kill -TERM <pid>
|
|
|
56
56
|
## Commands
|
|
57
57
|
|
|
58
58
|
```sh
|
|
59
|
-
npx codex-relay
|
|
59
|
+
npx codex-relay@latest
|
|
60
60
|
```
|
|
61
61
|
|
|
62
62
|
Start the relay in the foreground.
|
|
63
63
|
|
|
64
64
|
```sh
|
|
65
|
-
npx codex-relay --bg
|
|
65
|
+
npx codex-relay@latest --bg
|
|
66
66
|
```
|
|
67
67
|
|
|
68
68
|
Start the relay in the background.
|
|
69
69
|
|
|
70
70
|
```sh
|
|
71
|
-
npx codex-relay qr
|
|
71
|
+
npx codex-relay@latest qr
|
|
72
72
|
```
|
|
73
73
|
|
|
74
74
|
Print the latest pairing QR for an already running relay.
|
|
75
75
|
|
|
76
76
|
```sh
|
|
77
|
-
npx codex-relay approve XXXX-XXXX
|
|
77
|
+
npx codex-relay@latest approve XXXX-XXXX
|
|
78
78
|
```
|
|
79
79
|
|
|
80
80
|
Approve a pending mobile pairing request.
|
|
@@ -83,29 +83,29 @@ Approve a pending mobile pairing request.
|
|
|
83
83
|
|
|
84
84
|
The relay listens on `0.0.0.0:8787` by default. Configure it with environment variables:
|
|
85
85
|
|
|
86
|
-
| Variable | Purpose
|
|
87
|
-
| ----------------------------- |
|
|
88
|
-
| `PORT` | Server port. Defaults to `8787`.
|
|
89
|
-
| `HOST` | Listen host. Defaults to `0.0.0.0`.
|
|
90
|
-
| `CODEX_RELAY_PUBLIC_URL` | URL printed into the pairing QR, for example a Tailscale or tunnel URL.
|
|
91
|
-
| `CODEX_RELAY_WORKSPACE_PATH` | Workspace path Codex should use. Defaults to the directory where you run `npx codex-relay`. |
|
|
92
|
-
| `CODEX_RELAY_AUTH_DB_PATH` | Pairing and session database path. Defaults to `.codex-relay/auth.db`.
|
|
93
|
-
| `CODEX_RELAY_APPROVAL_SECRET` | Secret used by the local approve command. Usually generated automatically.
|
|
94
|
-
| `CODEX_HOME` | Codex home directory, used when reading Codex session metadata.
|
|
95
|
-
| `CODEX_BIN` | Codex CLI executable path.
|
|
86
|
+
| Variable | Purpose |
|
|
87
|
+
| ----------------------------- | -------------------------------------------------------------------------------------------------- |
|
|
88
|
+
| `PORT` | Server port. Defaults to `8787`. |
|
|
89
|
+
| `HOST` | Listen host. Defaults to `0.0.0.0`. |
|
|
90
|
+
| `CODEX_RELAY_PUBLIC_URL` | URL printed into the pairing QR, for example a Tailscale or tunnel URL. |
|
|
91
|
+
| `CODEX_RELAY_WORKSPACE_PATH` | Workspace path Codex should use. Defaults to the directory where you run `npx codex-relay@latest`. |
|
|
92
|
+
| `CODEX_RELAY_AUTH_DB_PATH` | Pairing and session database path. Defaults to `.codex-relay/auth.db`. |
|
|
93
|
+
| `CODEX_RELAY_APPROVAL_SECRET` | Secret used by the local approve command. Usually generated automatically. |
|
|
94
|
+
| `CODEX_HOME` | Codex home directory, used when reading Codex session metadata. |
|
|
95
|
+
| `CODEX_BIN` | Codex CLI executable path. |
|
|
96
96
|
|
|
97
97
|
Examples:
|
|
98
98
|
|
|
99
99
|
```sh
|
|
100
|
-
PORT=8788 npx codex-relay
|
|
100
|
+
PORT=8788 npx codex-relay@latest
|
|
101
101
|
```
|
|
102
102
|
|
|
103
103
|
```sh
|
|
104
|
-
CODEX_RELAY_PUBLIC_URL=http://100.64.0.10:8787 npx codex-relay
|
|
104
|
+
CODEX_RELAY_PUBLIC_URL=http://100.64.0.10:8787 npx codex-relay@latest
|
|
105
105
|
```
|
|
106
106
|
|
|
107
107
|
```sh
|
|
108
|
-
CODEX_RELAY_WORKSPACE_PATH=/path/to/project npx codex-relay
|
|
108
|
+
CODEX_RELAY_WORKSPACE_PATH=/path/to/project npx codex-relay@latest
|
|
109
109
|
```
|
|
110
110
|
|
|
111
111
|
## Network Notes
|
|
@@ -118,16 +118,16 @@ The phone must be able to reach the URL printed as `Mobile:` by the relay.
|
|
|
118
118
|
|
|
119
119
|
## Troubleshooting
|
|
120
120
|
|
|
121
|
-
If `npx codex-relay qr` cannot find a server, start one first:
|
|
121
|
+
If `npx codex-relay@latest qr` cannot find a server, start one first:
|
|
122
122
|
|
|
123
123
|
```sh
|
|
124
|
-
npx codex-relay
|
|
124
|
+
npx codex-relay@latest
|
|
125
125
|
```
|
|
126
126
|
|
|
127
127
|
If the relay says another process is using the local pairing database, use the existing server:
|
|
128
128
|
|
|
129
129
|
```sh
|
|
130
|
-
npx codex-relay qr
|
|
130
|
+
npx codex-relay@latest qr
|
|
131
131
|
```
|
|
132
132
|
|
|
133
133
|
Or stop the background process shown by the CLI:
|
package/dist/cli.js
CHANGED
|
@@ -31,7 +31,7 @@ async function startBackgroundServer() {
|
|
|
31
31
|
if (existingPid) {
|
|
32
32
|
console.log(`codex-relay is already running in the background (pid ${existingPid}).`);
|
|
33
33
|
console.log(`Logs: ${logPath}`);
|
|
34
|
-
console.log("Print the current pairing QR with: npx codex-relay qr");
|
|
34
|
+
console.log("Print the current pairing QR with: npx codex-relay@latest qr");
|
|
35
35
|
return;
|
|
36
36
|
}
|
|
37
37
|
await unlink(pidPath).catch(() => void 0);
|
|
@@ -66,7 +66,7 @@ async function startBackgroundServer() {
|
|
|
66
66
|
}
|
|
67
67
|
console.log(`Started codex-relay in the background (pid ${startedPid}).`);
|
|
68
68
|
console.log(`Logs: ${logPath}`);
|
|
69
|
-
console.log("Print the pairing QR later with: npx codex-relay qr");
|
|
69
|
+
console.log("Print the pairing QR later with: npx codex-relay@latest qr");
|
|
70
70
|
}
|
|
71
71
|
async function readRunningPid(pidPath) {
|
|
72
72
|
const value = await readFile(pidPath, "utf8").catch(() => void 0);
|
|
@@ -126,8 +126,8 @@ async function printPairingQr() {
|
|
|
126
126
|
const state = storedState?.pairingPayload ? storedState : await readServerLogState();
|
|
127
127
|
if (!state?.pairingPayload) {
|
|
128
128
|
console.error("No running Codex Relay server state was found.");
|
|
129
|
-
console.error("Start the server first with: npx codex-relay");
|
|
130
|
-
console.error("Or run it in the background with: npx codex-relay --bg");
|
|
129
|
+
console.error("Start the server first with: npx codex-relay@latest");
|
|
130
|
+
console.error("Or run it in the background with: npx codex-relay@latest --bg");
|
|
131
131
|
process.exitCode = 1;
|
|
132
132
|
return;
|
|
133
133
|
}
|
|
@@ -152,7 +152,7 @@ async function handleServerStartError(error) {
|
|
|
152
152
|
if (existingPid) {
|
|
153
153
|
console.error(`A background server appears to be running (pid ${existingPid}).`);
|
|
154
154
|
console.error("Use the existing server instead of starting a second one:");
|
|
155
|
-
console.error(" npx codex-relay qr");
|
|
155
|
+
console.error(" npx codex-relay@latest qr");
|
|
156
156
|
console.error("");
|
|
157
157
|
console.error("To stop the background server:");
|
|
158
158
|
console.error(` kill -TERM ${existingPid}`);
|
|
@@ -162,7 +162,7 @@ async function handleServerStartError(error) {
|
|
|
162
162
|
console.error("Another Codex Relay process is already running or exited without cleanup.");
|
|
163
163
|
if (state?.pairingPayload) {
|
|
164
164
|
console.error("Use the existing server instead of starting a second one:");
|
|
165
|
-
console.error(" npx codex-relay qr");
|
|
165
|
+
console.error(" npx codex-relay@latest qr");
|
|
166
166
|
console.error("");
|
|
167
167
|
}
|
|
168
168
|
console.error("Find it with:");
|
|
@@ -174,7 +174,7 @@ async function handleServerStartError(error) {
|
|
|
174
174
|
}
|
|
175
175
|
console.error("");
|
|
176
176
|
console.error("If you wanted a persistent server, start it once with:");
|
|
177
|
-
console.error(" npx codex-relay --bg");
|
|
177
|
+
console.error(" npx codex-relay@latest --bg");
|
|
178
178
|
process.exitCode = 1;
|
|
179
179
|
}
|
|
180
180
|
async function readApprovalSecret() {
|
package/dist/src.js
CHANGED
|
@@ -181,8 +181,9 @@ const WorkspaceChangesResponseSchema = z.object({
|
|
|
181
181
|
worktreeStatus: z.string().nullable()
|
|
182
182
|
})).default([])
|
|
183
183
|
});
|
|
184
|
-
const
|
|
185
|
-
const
|
|
184
|
+
const WorkspaceSelectionRequestSchema = z.object({ workspacePath: z.string().trim().min(1).optional() });
|
|
185
|
+
const CheckoutWorkspaceBranchRequestSchema = WorkspaceSelectionRequestSchema.extend({ branch: z.string().trim().min(1).refine((branch) => !branch.startsWith("-"), "Branch name cannot start with '-'.") });
|
|
186
|
+
const CommitPushWorkspaceRequestSchema = WorkspaceSelectionRequestSchema.extend({ message: z.string().trim().min(1).max(240) });
|
|
186
187
|
const WorkspaceGitActionResponseSchema = z.object({
|
|
187
188
|
branch: z.string().nullable(),
|
|
188
189
|
message: z.string().min(1),
|
|
@@ -206,6 +207,7 @@ const WebPreviewTargetSchema = z.object({
|
|
|
206
207
|
detectedAt: IsoDateTimeSchema
|
|
207
208
|
});
|
|
208
209
|
const PairRequestSchema = z.object({
|
|
210
|
+
clientSessionId: z.string().trim().min(1).max(120).optional(),
|
|
209
211
|
clientName: z.string().trim().min(1).max(80).optional(),
|
|
210
212
|
secure: z.object({
|
|
211
213
|
clientEphemeralPublicKey: z.string().min(1),
|
package/dist/src2.js
CHANGED
|
@@ -540,7 +540,8 @@ function createApp(options = {}) {
|
|
|
540
540
|
const tokenHash = token ? options.pairing.hashClientToken(token) : void 0;
|
|
541
541
|
const validSession = tokenHash ? await options.pairing.sessions.getValidSession(tokenHash, Date.now()) : void 0;
|
|
542
542
|
if (!tokenHash || !validSession) return c.json(apiError("unauthorized", "Pair this device with the Codex Relay server."), 401);
|
|
543
|
-
if (options.pairing.serverIdentity && !secureSessionsByTokenHash.has(tokenHash))
|
|
543
|
+
if (options.pairing.serverIdentity && !secureSessionsByTokenHash.has(tokenHash)) if (validSession.secureSession) secureSessionsByTokenHash.set(tokenHash, validSession.secureSession);
|
|
544
|
+
else return c.json(apiError("secure_session_required", "Secure session expired. Pair this device again."), 401);
|
|
544
545
|
await next();
|
|
545
546
|
});
|
|
546
547
|
app.post(apiPaths.pair, async (c) => {
|
|
@@ -556,6 +557,7 @@ function createApp(options = {}) {
|
|
|
556
557
|
approved: false,
|
|
557
558
|
approvalCode,
|
|
558
559
|
clientEphemeralPublicKey: parsed.data.secure.clientEphemeralPublicKey,
|
|
560
|
+
clientSessionId: parsed.data.clientSessionId,
|
|
559
561
|
clientName: parsed.data.clientName,
|
|
560
562
|
clientNonce: parsed.data.secure.clientNonce,
|
|
561
563
|
expiresAt,
|
|
@@ -584,15 +586,6 @@ function createApp(options = {}) {
|
|
|
584
586
|
const clientToken = options.pairing.createClientToken();
|
|
585
587
|
const expiresAt = Date.now() + options.pairing.tokenTtlMs;
|
|
586
588
|
const tokenHash = options.pairing.hashClientToken(clientToken);
|
|
587
|
-
const tokenCount = await options.pairing.sessions.createSession(tokenHash, {
|
|
588
|
-
clientName: pending.clientName,
|
|
589
|
-
expiresAt
|
|
590
|
-
});
|
|
591
|
-
await options.pairing.sessions.deletePendingPairing(approvalCode);
|
|
592
|
-
options.pairing.onPaired?.({
|
|
593
|
-
clientName: pending.clientName,
|
|
594
|
-
tokenCount
|
|
595
|
-
});
|
|
596
589
|
const clientTokenExpiresAt = new Date(expiresAt).toISOString();
|
|
597
590
|
const pairing = createSecurePairing({
|
|
598
591
|
approvalCode,
|
|
@@ -604,6 +597,17 @@ function createApp(options = {}) {
|
|
|
604
597
|
serverIdentity: options.pairing.serverIdentity,
|
|
605
598
|
serverUrl: pending.serverUrl
|
|
606
599
|
});
|
|
600
|
+
const tokenCount = await options.pairing.sessions.createSession(tokenHash, {
|
|
601
|
+
clientSessionId: pending.clientSessionId,
|
|
602
|
+
clientName: pending.clientName,
|
|
603
|
+
expiresAt,
|
|
604
|
+
secureSession: pairing.session
|
|
605
|
+
});
|
|
606
|
+
await options.pairing.sessions.deletePendingPairing(approvalCode);
|
|
607
|
+
options.pairing.onPaired?.({
|
|
608
|
+
clientName: pending.clientName,
|
|
609
|
+
tokenCount
|
|
610
|
+
});
|
|
607
611
|
secureSessionsByTokenHash.set(tokenHash, pairing.session);
|
|
608
612
|
return c.json(PairResponseSchema.parse({ secure: pairing.response }), 201);
|
|
609
613
|
});
|
|
@@ -630,15 +634,15 @@ function createApp(options = {}) {
|
|
|
630
634
|
const expiresAt = Date.now() + options.pairing.tokenTtlMs;
|
|
631
635
|
const oldTokenHash = options.pairing.hashClientToken(oldToken);
|
|
632
636
|
const newTokenHash = options.pairing.hashClientToken(clientToken);
|
|
637
|
+
const clientSessionId = normalizeClientSessionId(c.req.header("x-codex-relay-client-session-id")) ?? oldSession.clientSessionId;
|
|
633
638
|
const tokenCount = await options.pairing.sessions.rotateSession(oldTokenHash, newTokenHash, {
|
|
639
|
+
clientSessionId,
|
|
634
640
|
clientName: oldSession.clientName,
|
|
635
|
-
expiresAt
|
|
641
|
+
expiresAt,
|
|
642
|
+
secureSession: secureSessionsByTokenHash.get(oldTokenHash) ?? oldSession.secureSession
|
|
636
643
|
});
|
|
637
|
-
const secureSession = secureSessionsByTokenHash.get(oldTokenHash);
|
|
638
|
-
if (secureSession)
|
|
639
|
-
secureSessionsByTokenHash.delete(oldTokenHash);
|
|
640
|
-
secureSessionsByTokenHash.set(newTokenHash, secureSession);
|
|
641
|
-
}
|
|
644
|
+
const secureSession = secureSessionsByTokenHash.get(oldTokenHash) ?? oldSession.secureSession;
|
|
645
|
+
if (secureSession) secureSessionsByTokenHash.set(oldTokenHash, secureSession);
|
|
642
646
|
options.pairing.onTokenRefreshed?.({
|
|
643
647
|
clientName: oldSession.clientName,
|
|
644
648
|
tokenCount
|
|
@@ -647,7 +651,13 @@ function createApp(options = {}) {
|
|
|
647
651
|
clientToken,
|
|
648
652
|
clientTokenExpiresAt: new Date(expiresAt).toISOString()
|
|
649
653
|
});
|
|
650
|
-
|
|
654
|
+
const jsonResponse = await secureJson(c, options.pairing, secureSessionsByTokenHash, response, 201);
|
|
655
|
+
if (secureSession) {
|
|
656
|
+
secureSessionsByTokenHash.delete(oldTokenHash);
|
|
657
|
+
secureSessionsByTokenHash.set(newTokenHash, secureSession);
|
|
658
|
+
await options.pairing.sessions.updateSecureSession(newTokenHash, secureSession);
|
|
659
|
+
}
|
|
660
|
+
return jsonResponse;
|
|
651
661
|
});
|
|
652
662
|
app.get(apiPaths.status, (c) => {
|
|
653
663
|
const response = StatusResponseSchema.parse({
|
|
@@ -682,9 +692,11 @@ function createApp(options = {}) {
|
|
|
682
692
|
});
|
|
683
693
|
app.get(apiPaths.workspaceChanges, async (c) => {
|
|
684
694
|
try {
|
|
685
|
-
const
|
|
695
|
+
const selectedWorkspacePath = await validateThreadWorkspacePath(workspacePath, c.req.query("workspacePath"));
|
|
696
|
+
if (!selectedWorkspacePath.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("invalid_workspace_path", selectedWorkspacePath.error), 400);
|
|
697
|
+
const changes = await readWorkspaceChanges(selectedWorkspacePath.path);
|
|
686
698
|
const response = WorkspaceChangesResponseSchema.parse({
|
|
687
|
-
workspacePath,
|
|
699
|
+
workspacePath: selectedWorkspacePath.path,
|
|
688
700
|
...changes
|
|
689
701
|
});
|
|
690
702
|
return secureJson(c, options.pairing, secureSessionsByTokenHash, response);
|
|
@@ -696,10 +708,17 @@ function createApp(options = {}) {
|
|
|
696
708
|
const parsed = await parseRequestJson(c, options.pairing, secureSessionsByTokenHash, CheckoutWorkspaceBranchRequestSchema);
|
|
697
709
|
if (!parsed.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, validationError(parsed.error), 400);
|
|
698
710
|
try {
|
|
699
|
-
const
|
|
711
|
+
const selectedWorkspacePath = await validateThreadWorkspacePath(workspacePath, parsed.data.workspacePath);
|
|
712
|
+
if (!selectedWorkspacePath.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("invalid_workspace_path", selectedWorkspacePath.error), 400);
|
|
713
|
+
const branchExists = await localGitBranchExists(selectedWorkspacePath.path, parsed.data.branch);
|
|
714
|
+
const output = branchExists ? await git(selectedWorkspacePath.path, ["checkout", parsed.data.branch]) : await git(selectedWorkspacePath.path, [
|
|
715
|
+
"checkout",
|
|
716
|
+
"-b",
|
|
717
|
+
parsed.data.branch
|
|
718
|
+
]);
|
|
700
719
|
const response = WorkspaceGitActionResponseSchema.parse({
|
|
701
|
-
branch: await currentGitBranch(
|
|
702
|
-
message: `Checked out ${parsed.data.branch}.`,
|
|
720
|
+
branch: await currentGitBranch(selectedWorkspacePath.path),
|
|
721
|
+
message: branchExists ? `Checked out ${parsed.data.branch}.` : `Created and checked out ${parsed.data.branch}.`,
|
|
703
722
|
output
|
|
704
723
|
});
|
|
705
724
|
return secureJson(c, options.pairing, secureSessionsByTokenHash, response);
|
|
@@ -711,23 +730,25 @@ function createApp(options = {}) {
|
|
|
711
730
|
const parsed = await parseRequestJson(c, options.pairing, secureSessionsByTokenHash, CommitPushWorkspaceRequestSchema);
|
|
712
731
|
if (!parsed.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, validationError(parsed.error), 400);
|
|
713
732
|
try {
|
|
714
|
-
await
|
|
715
|
-
|
|
733
|
+
const selectedWorkspacePath = await validateThreadWorkspacePath(workspacePath, parsed.data.workspacePath);
|
|
734
|
+
if (!selectedWorkspacePath.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("invalid_workspace_path", selectedWorkspacePath.error), 400);
|
|
735
|
+
await git(selectedWorkspacePath.path, ["add", "--all"]);
|
|
736
|
+
const commitOutput = await git(selectedWorkspacePath.path, [
|
|
716
737
|
"commit",
|
|
717
738
|
"-m",
|
|
718
739
|
parsed.data.message
|
|
719
740
|
]);
|
|
720
|
-
const branch = await currentGitBranch(
|
|
721
|
-
const pushOutput = await git(
|
|
741
|
+
const branch = await currentGitBranch(selectedWorkspacePath.path);
|
|
742
|
+
const pushOutput = await git(selectedWorkspacePath.path, [
|
|
722
743
|
"rev-parse",
|
|
723
744
|
"--abbrev-ref",
|
|
724
745
|
"@{upstream}"
|
|
725
|
-
]).catch(() => null) ? await git(
|
|
746
|
+
]).catch(() => null) ? await git(selectedWorkspacePath.path, ["push"]) : branch ? await git(selectedWorkspacePath.path, [
|
|
726
747
|
"push",
|
|
727
748
|
"-u",
|
|
728
749
|
"origin",
|
|
729
750
|
branch
|
|
730
|
-
]) : await git(
|
|
751
|
+
]) : await git(selectedWorkspacePath.path, ["push"]);
|
|
731
752
|
const response = WorkspaceGitActionResponseSchema.parse({
|
|
732
753
|
branch,
|
|
733
754
|
message: "Committed and pushed workspace changes.",
|
|
@@ -981,6 +1002,10 @@ function normalizeApprovalCode(value) {
|
|
|
981
1002
|
const normalized = value.toUpperCase().replace(/[^A-Z0-9]/g, "").replaceAll("O", "0").replaceAll("I", "1");
|
|
982
1003
|
return normalized.length === 8 ? `${normalized.slice(0, 4)}-${normalized.slice(4)}` : normalized;
|
|
983
1004
|
}
|
|
1005
|
+
function normalizeClientSessionId(value) {
|
|
1006
|
+
const trimmed = value?.trim();
|
|
1007
|
+
return trimmed ? trimmed.slice(0, 120) : void 0;
|
|
1008
|
+
}
|
|
984
1009
|
async function validateThreadWorkspacePath(rootPath, requestedPath) {
|
|
985
1010
|
const resolved = resolve(requestedPath ?? rootPath);
|
|
986
1011
|
try {
|
|
@@ -1513,7 +1538,8 @@ async function parseRequestJson(c, pairing, secureSessionsByTokenHash, schema) {
|
|
|
1513
1538
|
const envelope = EncryptedPayloadSchema.safeParse(payload);
|
|
1514
1539
|
if (!envelope.success) return schema.safeParse({ __invalidEncryptedPayload: true });
|
|
1515
1540
|
try {
|
|
1516
|
-
payload = JSON.parse(decryptFromMobile(secureSession, envelope.data));
|
|
1541
|
+
payload = JSON.parse(decryptFromMobile(secureSession.session, envelope.data));
|
|
1542
|
+
await secureSession.persist();
|
|
1517
1543
|
} catch {
|
|
1518
1544
|
payload = { __invalidEncryptedPayload: true };
|
|
1519
1545
|
}
|
|
@@ -1529,15 +1555,30 @@ async function parsePlainJson(request, schema) {
|
|
|
1529
1555
|
}
|
|
1530
1556
|
return schema.safeParse(payload);
|
|
1531
1557
|
}
|
|
1532
|
-
function secureJson(c, pairing, secureSessionsByTokenHash, payload, status) {
|
|
1558
|
+
async function secureJson(c, pairing, secureSessionsByTokenHash, payload, status) {
|
|
1533
1559
|
const secureSession = getSecureSessionForRequest(c, pairing, secureSessionsByTokenHash);
|
|
1534
1560
|
if (!secureSession) return c.json(payload, status);
|
|
1535
|
-
const encrypted = EncryptedPayloadSchema.parse(encryptForMobile(secureSession, JSON.stringify(payload)));
|
|
1561
|
+
const encrypted = EncryptedPayloadSchema.parse(encryptForMobile(secureSession.session, JSON.stringify(payload)));
|
|
1562
|
+
await secureSession.persist();
|
|
1536
1563
|
return c.json(encrypted, status);
|
|
1537
1564
|
}
|
|
1538
1565
|
function getSecureSessionForRequest(c, pairing, secureSessionsByTokenHash) {
|
|
1539
1566
|
const token = parseBearerToken(c.req.header("authorization"));
|
|
1540
|
-
|
|
1567
|
+
if (!token || !pairing) return;
|
|
1568
|
+
const tokenHash = pairing.hashClientToken(token);
|
|
1569
|
+
const session = secureSessionsByTokenHash.get(tokenHash);
|
|
1570
|
+
return session ? createSecureSessionHandle(pairing, tokenHash, session) : void 0;
|
|
1571
|
+
}
|
|
1572
|
+
function createSecureSessionHandle(pairing, tokenHash, session) {
|
|
1573
|
+
let pendingPersist = Promise.resolve();
|
|
1574
|
+
return {
|
|
1575
|
+
persist: () => {
|
|
1576
|
+
pendingPersist = pendingPersist.catch(() => void 0).then(() => pairing.sessions.updateSecureSession(tokenHash, session));
|
|
1577
|
+
return pendingPersist;
|
|
1578
|
+
},
|
|
1579
|
+
session,
|
|
1580
|
+
tokenHash
|
|
1581
|
+
};
|
|
1541
1582
|
}
|
|
1542
1583
|
function sortedThreads(threads) {
|
|
1543
1584
|
return [...threads.values()].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
@@ -1664,7 +1705,8 @@ function updateThread(threads, messagesByThreadId, threadId, update) {
|
|
|
1664
1705
|
}
|
|
1665
1706
|
function sendSse(controller, encoder, secureSession, event) {
|
|
1666
1707
|
const parsed = StreamThreadRunEventSchema.parse(event);
|
|
1667
|
-
const data = secureSession ? EncryptedPayloadSchema.parse(encryptForMobile(secureSession, JSON.stringify(parsed))) : parsed;
|
|
1708
|
+
const data = secureSession ? EncryptedPayloadSchema.parse(encryptForMobile(secureSession.session, JSON.stringify(parsed))) : parsed;
|
|
1709
|
+
if (secureSession) secureSession.persist().catch(() => void 0);
|
|
1668
1710
|
controller.enqueue(encoder.encode(`event: ${parsed.type}\n`));
|
|
1669
1711
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`));
|
|
1670
1712
|
}
|
|
@@ -2417,7 +2459,7 @@ function normalizeFileChanges(value) {
|
|
|
2417
2459
|
async function readWorkspaceChanges(workspacePath) {
|
|
2418
2460
|
const repo = await openRepository(workspacePath);
|
|
2419
2461
|
const [currentBranch, branches] = await Promise.all([currentGitBranch(workspacePath), listGitBranches(workspacePath)]);
|
|
2420
|
-
const statusEntries = collectIterator(repo.statuses().iter());
|
|
2462
|
+
const statusEntries = collectIterator(repo.statuses().iter()).filter((entry) => !entry.status().ignored);
|
|
2421
2463
|
const statusByPath = new Map(statusEntries.map((entry) => [entry.path(), entry]));
|
|
2422
2464
|
const status = statusEntries.map((entry) => formatStatusLine(entry.path(), entry.status())).join("\n");
|
|
2423
2465
|
const structuredDiff = createWorkspaceDiff(repo);
|
|
@@ -2465,6 +2507,11 @@ async function readWorkspaceChanges(workspacePath) {
|
|
|
2465
2507
|
});
|
|
2466
2508
|
}
|
|
2467
2509
|
const files = [...filesByPath.values()].sort((left, right) => left.path.localeCompare(right.path));
|
|
2510
|
+
const fileStats = {
|
|
2511
|
+
additions: files.reduce((total, file) => total + file.additions, 0),
|
|
2512
|
+
deletions: files.reduce((total, file) => total + file.deletions, 0),
|
|
2513
|
+
filesChanged: files.length
|
|
2514
|
+
};
|
|
2468
2515
|
return {
|
|
2469
2516
|
branches,
|
|
2470
2517
|
currentBranch,
|
|
@@ -2472,16 +2519,29 @@ async function readWorkspaceChanges(workspacePath) {
|
|
|
2472
2519
|
files,
|
|
2473
2520
|
hasChanges: files.length > 0 || Boolean(status.trim() || diff.trim()),
|
|
2474
2521
|
status,
|
|
2475
|
-
stats: {
|
|
2522
|
+
stats: files.length > 0 ? fileStats : {
|
|
2476
2523
|
additions: Number(stats.insertions),
|
|
2477
2524
|
deletions: Number(stats.deletions),
|
|
2478
|
-
filesChanged:
|
|
2525
|
+
filesChanged: Number(stats.filesChanged)
|
|
2479
2526
|
}
|
|
2480
2527
|
};
|
|
2481
2528
|
}
|
|
2482
2529
|
async function currentGitBranch(workspacePath) {
|
|
2483
2530
|
return (await git(workspacePath, ["branch", "--show-current"]).catch(() => "")).trim() || null;
|
|
2484
2531
|
}
|
|
2532
|
+
async function localGitBranchExists(workspacePath, branch) {
|
|
2533
|
+
try {
|
|
2534
|
+
await git(workspacePath, [
|
|
2535
|
+
"show-ref",
|
|
2536
|
+
"--verify",
|
|
2537
|
+
"--quiet",
|
|
2538
|
+
`refs/heads/${branch}`
|
|
2539
|
+
]);
|
|
2540
|
+
return true;
|
|
2541
|
+
} catch {
|
|
2542
|
+
return false;
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2485
2545
|
async function listGitBranches(workspacePath) {
|
|
2486
2546
|
return (await git(workspacePath, [
|
|
2487
2547
|
"branch",
|
|
@@ -2596,14 +2656,21 @@ async function createTursoPairingSessionStore(path) {
|
|
|
2596
2656
|
await db.exec(`
|
|
2597
2657
|
CREATE TABLE IF NOT EXISTS pairing_sessions (
|
|
2598
2658
|
token_hash TEXT PRIMARY KEY,
|
|
2659
|
+
client_session_id TEXT,
|
|
2599
2660
|
client_name TEXT,
|
|
2600
2661
|
expires_at INTEGER NOT NULL,
|
|
2662
|
+
key_epoch INTEGER,
|
|
2663
|
+
mobile_to_server_key TEXT,
|
|
2664
|
+
server_to_mobile_key TEXT,
|
|
2665
|
+
last_mobile_counter INTEGER,
|
|
2666
|
+
next_server_counter INTEGER,
|
|
2601
2667
|
created_at INTEGER NOT NULL,
|
|
2602
2668
|
updated_at INTEGER NOT NULL
|
|
2603
2669
|
);
|
|
2604
2670
|
|
|
2605
2671
|
CREATE TABLE IF NOT EXISTS pending_pairings (
|
|
2606
2672
|
approval_code TEXT PRIMARY KEY,
|
|
2673
|
+
client_session_id TEXT,
|
|
2607
2674
|
client_name TEXT,
|
|
2608
2675
|
client_ephemeral_public_key TEXT NOT NULL,
|
|
2609
2676
|
client_nonce TEXT NOT NULL,
|
|
@@ -2614,8 +2681,9 @@ async function createTursoPairingSessionStore(path) {
|
|
|
2614
2681
|
updated_at INTEGER NOT NULL
|
|
2615
2682
|
);
|
|
2616
2683
|
`);
|
|
2684
|
+
await ensurePairingSessionColumns();
|
|
2617
2685
|
async function countActive(now) {
|
|
2618
|
-
const row = await db.prepare("SELECT COUNT(
|
|
2686
|
+
const row = await db.prepare("SELECT COUNT(DISTINCT COALESCE(client_session_id, token_hash)) AS count FROM pairing_sessions WHERE expires_at > ?").get(now);
|
|
2619
2687
|
return Number(row?.count ?? 0);
|
|
2620
2688
|
}
|
|
2621
2689
|
async function deleteSession(tokenHash) {
|
|
@@ -2626,6 +2694,7 @@ async function createTursoPairingSessionStore(path) {
|
|
|
2626
2694
|
}
|
|
2627
2695
|
async function getPendingPairing(approvalCode, now) {
|
|
2628
2696
|
const row = await db.prepare(`SELECT approval_code AS approvalCode,
|
|
2697
|
+
client_session_id AS clientSessionId,
|
|
2629
2698
|
client_name AS clientName,
|
|
2630
2699
|
client_ephemeral_public_key AS clientEphemeralPublicKey,
|
|
2631
2700
|
client_nonce AS clientNonce,
|
|
@@ -2644,6 +2713,7 @@ async function createTursoPairingSessionStore(path) {
|
|
|
2644
2713
|
approvalCode: String(row.approvalCode),
|
|
2645
2714
|
approved: Number(row.approved) === 1,
|
|
2646
2715
|
clientEphemeralPublicKey: String(row.clientEphemeralPublicKey),
|
|
2716
|
+
clientSessionId: typeof row.clientSessionId === "string" ? row.clientSessionId : void 0,
|
|
2647
2717
|
clientName: typeof row.clientName === "string" ? row.clientName : void 0,
|
|
2648
2718
|
clientNonce: String(row.clientNonce),
|
|
2649
2719
|
expiresAt,
|
|
@@ -2665,6 +2735,7 @@ async function createTursoPairingSessionStore(path) {
|
|
|
2665
2735
|
const now = Date.now();
|
|
2666
2736
|
await db.prepare(`INSERT INTO pending_pairings (
|
|
2667
2737
|
approval_code,
|
|
2738
|
+
client_session_id,
|
|
2668
2739
|
client_name,
|
|
2669
2740
|
client_ephemeral_public_key,
|
|
2670
2741
|
client_nonce,
|
|
@@ -2674,19 +2745,45 @@ async function createTursoPairingSessionStore(path) {
|
|
|
2674
2745
|
created_at,
|
|
2675
2746
|
updated_at
|
|
2676
2747
|
)
|
|
2677
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(pairing.approvalCode, pairing.clientName ?? null, pairing.clientEphemeralPublicKey, pairing.clientNonce, pairing.serverUrl, pairing.approved ? 1 : 0, pairing.expiresAt, now, now);
|
|
2748
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(pairing.approvalCode, pairing.clientSessionId ?? null, pairing.clientName ?? null, pairing.clientEphemeralPublicKey, pairing.clientNonce, pairing.serverUrl, pairing.approved ? 1 : 0, pairing.expiresAt, now, now);
|
|
2678
2749
|
},
|
|
2679
2750
|
async createSession(tokenHash, session) {
|
|
2680
2751
|
const now = Date.now();
|
|
2681
|
-
|
|
2682
|
-
|
|
2752
|
+
const secure = encodeSecureSession(session.secureSession);
|
|
2753
|
+
if (session.clientSessionId) {
|
|
2754
|
+
await db.prepare("DELETE FROM pairing_sessions WHERE client_session_id = ?").run(session.clientSessionId);
|
|
2755
|
+
if (session.clientName) await db.prepare("DELETE FROM pairing_sessions WHERE client_session_id IS NULL AND client_name = ?").run(session.clientName);
|
|
2756
|
+
}
|
|
2757
|
+
await db.prepare(`INSERT INTO pairing_sessions (
|
|
2758
|
+
token_hash,
|
|
2759
|
+
client_session_id,
|
|
2760
|
+
client_name,
|
|
2761
|
+
expires_at,
|
|
2762
|
+
key_epoch,
|
|
2763
|
+
mobile_to_server_key,
|
|
2764
|
+
server_to_mobile_key,
|
|
2765
|
+
last_mobile_counter,
|
|
2766
|
+
next_server_counter,
|
|
2767
|
+
created_at,
|
|
2768
|
+
updated_at
|
|
2769
|
+
)
|
|
2770
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(tokenHash, session.clientSessionId ?? null, session.clientName ?? null, session.expiresAt, secure?.keyEpoch ?? null, secure?.mobileToServerKey ?? null, secure?.serverToMobileKey ?? null, secure?.lastMobileCounter ?? null, secure?.nextServerCounter ?? null, now, now);
|
|
2683
2771
|
return countActive(now);
|
|
2684
2772
|
},
|
|
2685
2773
|
deleteSession,
|
|
2686
2774
|
deletePendingPairing,
|
|
2687
2775
|
getPendingPairing,
|
|
2688
2776
|
async getValidSession(tokenHash, now) {
|
|
2689
|
-
const row = await db.prepare(
|
|
2777
|
+
const row = await db.prepare(`SELECT client_name AS clientName,
|
|
2778
|
+
client_session_id AS clientSessionId,
|
|
2779
|
+
expires_at AS expiresAt,
|
|
2780
|
+
key_epoch AS keyEpoch,
|
|
2781
|
+
mobile_to_server_key AS mobileToServerKey,
|
|
2782
|
+
server_to_mobile_key AS serverToMobileKey,
|
|
2783
|
+
last_mobile_counter AS lastMobileCounter,
|
|
2784
|
+
next_server_counter AS nextServerCounter
|
|
2785
|
+
FROM pairing_sessions
|
|
2786
|
+
WHERE token_hash = ?`).get(tokenHash);
|
|
2690
2787
|
if (!row) return;
|
|
2691
2788
|
const expiresAt = Number(row.expiresAt);
|
|
2692
2789
|
if (now > expiresAt) {
|
|
@@ -2694,8 +2791,10 @@ async function createTursoPairingSessionStore(path) {
|
|
|
2694
2791
|
return;
|
|
2695
2792
|
}
|
|
2696
2793
|
return {
|
|
2794
|
+
clientSessionId: typeof row.clientSessionId === "string" ? row.clientSessionId : void 0,
|
|
2697
2795
|
clientName: typeof row.clientName === "string" ? row.clientName : void 0,
|
|
2698
|
-
expiresAt
|
|
2796
|
+
expiresAt,
|
|
2797
|
+
secureSession: decodeSecureSession(row)
|
|
2699
2798
|
};
|
|
2700
2799
|
},
|
|
2701
2800
|
async pruneExpired(now) {
|
|
@@ -2704,14 +2803,82 @@ async function createTursoPairingSessionStore(path) {
|
|
|
2704
2803
|
},
|
|
2705
2804
|
async rotateSession(oldTokenHash, newTokenHash, session) {
|
|
2706
2805
|
const now = Date.now();
|
|
2806
|
+
const secure = encodeSecureSession(session.secureSession);
|
|
2707
2807
|
await db.transaction(async () => {
|
|
2708
2808
|
await db.prepare("DELETE FROM pairing_sessions WHERE token_hash = ?").run(oldTokenHash);
|
|
2709
|
-
|
|
2710
|
-
|
|
2809
|
+
if (session.clientSessionId) {
|
|
2810
|
+
await db.prepare("DELETE FROM pairing_sessions WHERE client_session_id = ?").run(session.clientSessionId);
|
|
2811
|
+
if (session.clientName) await db.prepare("DELETE FROM pairing_sessions WHERE client_session_id IS NULL AND client_name = ?").run(session.clientName);
|
|
2812
|
+
}
|
|
2813
|
+
await db.prepare(`INSERT INTO pairing_sessions (
|
|
2814
|
+
token_hash,
|
|
2815
|
+
client_session_id,
|
|
2816
|
+
client_name,
|
|
2817
|
+
expires_at,
|
|
2818
|
+
key_epoch,
|
|
2819
|
+
mobile_to_server_key,
|
|
2820
|
+
server_to_mobile_key,
|
|
2821
|
+
last_mobile_counter,
|
|
2822
|
+
next_server_counter,
|
|
2823
|
+
created_at,
|
|
2824
|
+
updated_at
|
|
2825
|
+
)
|
|
2826
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(newTokenHash, session.clientSessionId ?? null, session.clientName ?? null, session.expiresAt, secure?.keyEpoch ?? null, secure?.mobileToServerKey ?? null, secure?.serverToMobileKey ?? null, secure?.lastMobileCounter ?? null, secure?.nextServerCounter ?? null, now, now);
|
|
2711
2827
|
})();
|
|
2712
2828
|
return countActive(now);
|
|
2829
|
+
},
|
|
2830
|
+
async updateSecureSession(tokenHash, secureSession) {
|
|
2831
|
+
const secure = encodeSecureSession(secureSession);
|
|
2832
|
+
const now = Date.now();
|
|
2833
|
+
await db.prepare(`UPDATE pairing_sessions
|
|
2834
|
+
SET key_epoch = ?,
|
|
2835
|
+
mobile_to_server_key = ?,
|
|
2836
|
+
server_to_mobile_key = ?,
|
|
2837
|
+
last_mobile_counter = ?,
|
|
2838
|
+
next_server_counter = ?,
|
|
2839
|
+
updated_at = ?
|
|
2840
|
+
WHERE token_hash = ?`).run(secure.keyEpoch, secure.mobileToServerKey, secure.serverToMobileKey, secure.lastMobileCounter, secure.nextServerCounter, now, tokenHash);
|
|
2713
2841
|
}
|
|
2714
2842
|
};
|
|
2843
|
+
async function ensurePairingSessionColumns() {
|
|
2844
|
+
const rows = await db.prepare("PRAGMA table_info(pairing_sessions)").all();
|
|
2845
|
+
const columns = new Set(resultRows(rows).map((row) => String(row.name)));
|
|
2846
|
+
for (const [column, sql] of [
|
|
2847
|
+
["client_session_id", "ALTER TABLE pairing_sessions ADD COLUMN client_session_id TEXT"],
|
|
2848
|
+
["key_epoch", "ALTER TABLE pairing_sessions ADD COLUMN key_epoch INTEGER"],
|
|
2849
|
+
["mobile_to_server_key", "ALTER TABLE pairing_sessions ADD COLUMN mobile_to_server_key TEXT"],
|
|
2850
|
+
["server_to_mobile_key", "ALTER TABLE pairing_sessions ADD COLUMN server_to_mobile_key TEXT"],
|
|
2851
|
+
["last_mobile_counter", "ALTER TABLE pairing_sessions ADD COLUMN last_mobile_counter INTEGER"],
|
|
2852
|
+
["next_server_counter", "ALTER TABLE pairing_sessions ADD COLUMN next_server_counter INTEGER"]
|
|
2853
|
+
]) if (!columns.has(column)) await db.exec(sql);
|
|
2854
|
+
const pendingRows = await db.prepare("PRAGMA table_info(pending_pairings)").all();
|
|
2855
|
+
if (!new Set(resultRows(pendingRows).map((row) => String(row.name))).has("client_session_id")) await db.exec("ALTER TABLE pending_pairings ADD COLUMN client_session_id TEXT");
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
function encodeSecureSession(session) {
|
|
2859
|
+
if (!session) return;
|
|
2860
|
+
return {
|
|
2861
|
+
keyEpoch: session.keyEpoch,
|
|
2862
|
+
lastMobileCounter: session.lastMobileCounter,
|
|
2863
|
+
mobileToServerKey: fromByteArray(session.mobileToServerKey),
|
|
2864
|
+
nextServerCounter: session.nextServerCounter,
|
|
2865
|
+
serverToMobileKey: fromByteArray(session.serverToMobileKey)
|
|
2866
|
+
};
|
|
2867
|
+
}
|
|
2868
|
+
function decodeSecureSession(row) {
|
|
2869
|
+
if (typeof row.mobileToServerKey !== "string" || typeof row.serverToMobileKey !== "string" || row.keyEpoch === null || row.lastMobileCounter === null || row.nextServerCounter === null) return;
|
|
2870
|
+
return {
|
|
2871
|
+
keyEpoch: Number(row.keyEpoch),
|
|
2872
|
+
lastMobileCounter: Number(row.lastMobileCounter),
|
|
2873
|
+
mobileToServerKey: toByteArray(row.mobileToServerKey),
|
|
2874
|
+
nextServerCounter: Number(row.nextServerCounter),
|
|
2875
|
+
serverToMobileKey: toByteArray(row.serverToMobileKey)
|
|
2876
|
+
};
|
|
2877
|
+
}
|
|
2878
|
+
function resultRows(result) {
|
|
2879
|
+
if (Array.isArray(result)) return result;
|
|
2880
|
+
if (result && typeof result === "object" && Array.isArray(result.rows)) return result.rows;
|
|
2881
|
+
return [];
|
|
2715
2882
|
}
|
|
2716
2883
|
//#endregion
|
|
2717
2884
|
//#region src/index.ts
|
|
@@ -2846,7 +3013,7 @@ async function writeBackgroundPid() {
|
|
|
2846
3013
|
await writeFile(path, `${process.pid}\n`, { mode: 384 });
|
|
2847
3014
|
}
|
|
2848
3015
|
function formatApprovalCommand(approvalCode, activePort) {
|
|
2849
|
-
return activePort === 8787 ? `npx codex-relay approve ${approvalCode}` : `PORT=${activePort} npx codex-relay approve ${approvalCode}`;
|
|
3016
|
+
return activePort === 8787 ? `npx codex-relay@latest approve ${approvalCode}` : `PORT=${activePort} npx codex-relay@latest approve ${approvalCode}`;
|
|
2850
3017
|
}
|
|
2851
3018
|
function getLocalNetworkConnectUrl(port) {
|
|
2852
3019
|
for (const addresses of Object.values(networkInterfaces())) for (const address of addresses ?? []) if (address.family === "IPv4" && !address.internal) return `http://${address.address}:${port}`;
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-relay",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1-beta.1",
|
|
4
4
|
"description": "Local Codex Relay CLI bridge for the Codex Relay mobile app.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
|
-
"url": "git+https://github.com/gronxb/
|
|
7
|
+
"url": "git+https://github.com/gronxb/codex-relay.git",
|
|
8
8
|
"directory": "apps/server"
|
|
9
9
|
},
|
|
10
10
|
"bin": {
|