zk-credits 0.1.1 → 0.1.3

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 ADDED
@@ -0,0 +1,52 @@
1
+ # zk-credits
2
+
3
+ Loopback sidecar that attaches a ZK-RLN proof to each coding-agent LLM request.
4
+
5
+ ```bash
6
+ npm install --global zk-credits
7
+ ```
8
+
9
+ ## First run
10
+
11
+ 1. Fund an identity at the [web app](https://feature-zk-api-credits-gadillacers-projects.vercel.app)
12
+ (GitHub sign-in → generate 24-word phrase → buy Starter $1.00 / 100 tickets).
13
+ 2. Import the phrase (hidden TTY, OS keychain):
14
+
15
+ ```bash
16
+ zk-credits import-mnemonic
17
+ ```
18
+
19
+ 3. Run:
20
+
21
+ ```bash
22
+ zk-credits cline "summarize this repository"
23
+ zk-credits claude -p "summarize this repository"
24
+ zk-credits setup codex && zk-credits codex "summarize this repository"
25
+ ```
26
+
27
+ The sidecar binds `127.0.0.1:3210` only. It does not modify `~/.cline`,
28
+ `~/.claude`, or `~/.codex`.
29
+
30
+ ## Commands
31
+
32
+ ```
33
+ zk-credits cline [cline arguments...]
34
+ zk-credits claude [claude arguments...]
35
+ zk-credits setup codex [--model <model>]
36
+ zk-credits codex [codex arguments...]
37
+ zk-credits status
38
+ zk-credits import-mnemonic
39
+ zk-credits serve [--port <port>]
40
+ eval "$(zk-credits env)"
41
+ ```
42
+
43
+ Codex SDK:
44
+
45
+ ```ts
46
+ import { buildCodexSdkOptions, buildCodexThreadOptions } from 'zk-credits/codex';
47
+ ```
48
+
49
+ `ZK_CREDITS_MNEMONIC` is for a headless process only and is not persisted.
50
+
51
+ Testnet only. See the [repo README](https://github.com/mangekyou-labs/haze-api)
52
+ for caveats, tree capacity, and cold-start notes.
@@ -0,0 +1,81 @@
1
+ export interface AnthropicContentBlock {
2
+ type: string;
3
+ text?: string;
4
+ [key: string]: unknown;
5
+ }
6
+ export interface AnthropicMessage {
7
+ role: 'user' | 'assistant';
8
+ content: string | AnthropicContentBlock[];
9
+ }
10
+ export interface AnthropicMessagesRequest {
11
+ model?: string;
12
+ system?: string | AnthropicContentBlock[];
13
+ messages: AnthropicMessage[];
14
+ max_tokens?: number;
15
+ temperature?: number;
16
+ top_p?: number;
17
+ stop_sequences?: string[];
18
+ stream?: boolean;
19
+ [key: string]: unknown;
20
+ }
21
+ export interface OpenAiMessage {
22
+ role: 'system' | 'user' | 'assistant';
23
+ content: string;
24
+ }
25
+ export interface OpenAiChatRequest {
26
+ model: string;
27
+ messages: OpenAiMessage[];
28
+ max_tokens?: number;
29
+ temperature?: number;
30
+ top_p?: number;
31
+ stop?: string[];
32
+ stream?: boolean;
33
+ [key: string]: unknown;
34
+ }
35
+ export interface OpenAiChatResponse {
36
+ id?: string;
37
+ object?: string;
38
+ created?: number;
39
+ model?: string;
40
+ choices?: Array<{
41
+ index?: number;
42
+ message?: {
43
+ role?: string;
44
+ content?: string | null;
45
+ };
46
+ finish_reason?: string | null;
47
+ }>;
48
+ usage?: {
49
+ prompt_tokens?: number;
50
+ completion_tokens?: number;
51
+ total_tokens?: number;
52
+ };
53
+ }
54
+ export interface AnthropicMessageResponse {
55
+ id: string;
56
+ type: 'message';
57
+ role: 'assistant';
58
+ model: string;
59
+ content: Array<{
60
+ type: 'text';
61
+ text: string;
62
+ }>;
63
+ stop_reason: 'end_turn' | 'max_tokens' | 'stop_sequence' | 'tool_use' | null;
64
+ stop_sequence: string | null;
65
+ usage: {
66
+ input_tokens: number;
67
+ output_tokens: number;
68
+ };
69
+ }
70
+ export declare function anthropicErrorType(status: number): string;
71
+ export declare function translateAnthropicToOpenAi(request: AnthropicMessagesRequest): OpenAiChatRequest;
72
+ export declare function translateOpenAiToAnthropic(response: OpenAiChatResponse): AnthropicMessageResponse;
73
+ export interface StreamTransformerOptions {
74
+ model: string;
75
+ id?: string;
76
+ }
77
+ export interface AnthropicStreamTransformer {
78
+ transformChunk(chunk: unknown): string[];
79
+ finish(): string[];
80
+ }
81
+ export declare function createAnthropicStreamTransformer(options: StreamTransformerOptions): AnthropicStreamTransformer;
@@ -0,0 +1,192 @@
1
+ function normalizeContent(content) {
2
+ if (!content)
3
+ return '';
4
+ if (typeof content === 'string')
5
+ return content;
6
+ if (Array.isArray(content)) {
7
+ return content
8
+ .map((block) => {
9
+ if (typeof block === 'string')
10
+ return block;
11
+ if (block && typeof block.text === 'string')
12
+ return block.text;
13
+ return '';
14
+ })
15
+ .filter(Boolean)
16
+ .join('\n');
17
+ }
18
+ return '';
19
+ }
20
+ function resolveModel(requestedModel) {
21
+ if (!requestedModel)
22
+ return 'openai/gpt-4o-mini';
23
+ if (requestedModel.includes('/'))
24
+ return requestedModel;
25
+ return 'openai/gpt-4o-mini';
26
+ }
27
+ function mapFinishReason(reason) {
28
+ if (!reason || reason === 'stop')
29
+ return 'end_turn';
30
+ if (reason === 'length')
31
+ return 'max_tokens';
32
+ if (reason === 'tool_calls' || reason === 'function_call')
33
+ return 'tool_use';
34
+ return 'end_turn';
35
+ }
36
+ export function anthropicErrorType(status) {
37
+ switch (status) {
38
+ case 400:
39
+ return 'invalid_request_error';
40
+ case 401:
41
+ return 'authentication_error';
42
+ case 403:
43
+ return 'permission_error';
44
+ case 404:
45
+ return 'not_found_error';
46
+ case 413:
47
+ return 'request_too_large';
48
+ case 429:
49
+ return 'rate_limit_error';
50
+ case 529:
51
+ return 'overloaded_error';
52
+ default:
53
+ return status >= 500 ? 'api_error' : 'invalid_request_error';
54
+ }
55
+ }
56
+ export function translateAnthropicToOpenAi(request) {
57
+ const messages = [];
58
+ const systemContent = normalizeContent(request.system);
59
+ if (systemContent) {
60
+ messages.push({ role: 'system', content: systemContent });
61
+ }
62
+ for (const msg of request.messages || []) {
63
+ const content = normalizeContent(msg.content);
64
+ messages.push({
65
+ role: msg.role === 'assistant' ? 'assistant' : 'user',
66
+ content,
67
+ });
68
+ }
69
+ const openAiRequest = {
70
+ model: resolveModel(request.model),
71
+ messages,
72
+ };
73
+ if (request.max_tokens !== undefined) {
74
+ openAiRequest.max_tokens = request.max_tokens;
75
+ }
76
+ if (request.temperature !== undefined) {
77
+ openAiRequest.temperature = request.temperature;
78
+ }
79
+ if (request.top_p !== undefined) {
80
+ openAiRequest.top_p = request.top_p;
81
+ }
82
+ if (request.stop_sequences && request.stop_sequences.length > 0) {
83
+ openAiRequest.stop = request.stop_sequences;
84
+ }
85
+ if (request.stream) {
86
+ openAiRequest.stream = true;
87
+ }
88
+ return openAiRequest;
89
+ }
90
+ export function translateOpenAiToAnthropic(response) {
91
+ const choice = response.choices?.[0];
92
+ const text = choice?.message?.content ?? '';
93
+ const stopReason = mapFinishReason(choice?.finish_reason);
94
+ return {
95
+ id: response.id?.startsWith('msg_')
96
+ ? response.id
97
+ : `msg_${response.id || 'zk_' + Date.now()}`,
98
+ type: 'message',
99
+ role: 'assistant',
100
+ model: response.model || 'openai/gpt-4o-mini',
101
+ content: [{ type: 'text', text }],
102
+ stop_reason: stopReason,
103
+ stop_sequence: null,
104
+ usage: {
105
+ input_tokens: response.usage?.prompt_tokens ?? 0,
106
+ output_tokens: response.usage?.completion_tokens ?? 0,
107
+ },
108
+ };
109
+ }
110
+ export function createAnthropicStreamTransformer(options) {
111
+ const messageId = options.id || `msg_zk_${Date.now()}`;
112
+ const model = options.model;
113
+ let started = false;
114
+ let blockStarted = false;
115
+ let blockIndex = 0;
116
+ let promptTokens = 0;
117
+ let completionTokens = 0;
118
+ let finalStopReason = null;
119
+ function sse(event, data) {
120
+ return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
121
+ }
122
+ return {
123
+ transformChunk(rawChunk) {
124
+ const chunk = rawChunk;
125
+ const out = [];
126
+ if (!started) {
127
+ started = true;
128
+ out.push(sse('message_start', {
129
+ type: 'message_start',
130
+ message: {
131
+ id: messageId,
132
+ type: 'message',
133
+ role: 'assistant',
134
+ model,
135
+ content: [],
136
+ stop_reason: null,
137
+ stop_sequence: null,
138
+ usage: { input_tokens: 0, output_tokens: 0 },
139
+ },
140
+ }));
141
+ }
142
+ if (chunk.usage) {
143
+ if (chunk.usage.prompt_tokens)
144
+ promptTokens = chunk.usage.prompt_tokens;
145
+ if (chunk.usage.completion_tokens)
146
+ completionTokens = chunk.usage.completion_tokens;
147
+ }
148
+ const choice = chunk.choices?.[0];
149
+ const deltaContent = choice?.delta?.content;
150
+ if (deltaContent) {
151
+ if (!blockStarted) {
152
+ blockStarted = true;
153
+ out.push(sse('content_block_start', {
154
+ type: 'content_block_start',
155
+ index: blockIndex,
156
+ content_block: { type: 'text', text: '' },
157
+ }));
158
+ }
159
+ out.push(sse('content_block_delta', {
160
+ type: 'content_block_delta',
161
+ index: blockIndex,
162
+ delta: { type: 'text_delta', text: deltaContent },
163
+ }));
164
+ }
165
+ if (choice?.finish_reason) {
166
+ finalStopReason = mapFinishReason(choice.finish_reason);
167
+ }
168
+ return out;
169
+ },
170
+ finish() {
171
+ const out = [];
172
+ if (blockStarted) {
173
+ out.push(sse('content_block_stop', {
174
+ type: 'content_block_stop',
175
+ index: blockIndex,
176
+ }));
177
+ }
178
+ out.push(sse('message_delta', {
179
+ type: 'message_delta',
180
+ delta: {
181
+ stop_reason: finalStopReason || 'end_turn',
182
+ stop_sequence: null,
183
+ },
184
+ usage: { output_tokens: completionTokens },
185
+ }));
186
+ out.push(sse('message_stop', {
187
+ type: 'message_stop',
188
+ }));
189
+ return out;
190
+ },
191
+ };
192
+ }
@@ -0,0 +1,17 @@
1
+ interface SpawnedProcess {
2
+ once(event: 'error', listener: (error: NodeJS.ErrnoException) => void): this;
3
+ once(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;
4
+ }
5
+ export type ClaudeSpawnCommand = (command: string, args: readonly string[], options: {
6
+ stdio: 'inherit';
7
+ env: NodeJS.ProcessEnv;
8
+ }) => SpawnedProcess;
9
+ export interface LaunchClaudeOptions {
10
+ args: readonly string[];
11
+ loopbackBaseUrl: string;
12
+ localToken: string;
13
+ stateDirectory: string;
14
+ }
15
+ /** Configures and launches Claude Code against the proof-aware loopback transport. */
16
+ export declare function launchClaudeProcess(options: LaunchClaudeOptions, spawnImpl?: ClaudeSpawnCommand): Promise<number>;
17
+ export {};
@@ -0,0 +1,47 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { chmod, mkdir } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ const spawnCommand = (command, args, options) => spawn(command, [...args], options);
5
+ function anthropicBaseUrl(loopbackBaseUrl) {
6
+ const url = new URL(loopbackBaseUrl);
7
+ if (url.protocol !== 'http:' || url.hostname !== '127.0.0.1') {
8
+ throw new Error('Claude Code companion must use a 127.0.0.1 HTTP sidecar URL');
9
+ }
10
+ // Claude Code appends /v1/messages, so base URL must NOT have trailing /v1
11
+ return url.origin;
12
+ }
13
+ async function prepareClaudeDirectory(stateDirectory) {
14
+ const configDirectory = join(stateDirectory, 'claude');
15
+ await mkdir(configDirectory, { recursive: true, mode: 0o700 });
16
+ await chmod(configDirectory, 0o700);
17
+ return configDirectory;
18
+ }
19
+ function waitForClaude(child) {
20
+ return new Promise((resolve, reject) => {
21
+ child.once('error', (error) => {
22
+ if (error.code === 'ENOENT') {
23
+ reject(new Error('Claude Code CLI was not found; install it with npm install -g @anthropic-ai/claude-code before running zk-credits claude'));
24
+ return;
25
+ }
26
+ reject(error);
27
+ });
28
+ child.once('exit', (code) => resolve(code ?? 1));
29
+ });
30
+ }
31
+ /** Configures and launches Claude Code against the proof-aware loopback transport. */
32
+ export async function launchClaudeProcess(options, spawnImpl = spawnCommand) {
33
+ const configDirectory = await prepareClaudeDirectory(options.stateDirectory);
34
+ const baseUrl = anthropicBaseUrl(options.loopbackBaseUrl);
35
+ const env = {
36
+ ...process.env,
37
+ CLAUDE_CONFIG_DIR: configDirectory,
38
+ ANTHROPIC_BASE_URL: baseUrl,
39
+ ANTHROPIC_AUTH_TOKEN: options.localToken,
40
+ DISABLE_TELEMETRY: '1',
41
+ };
42
+ const child = spawnImpl('claude', [...options.args], {
43
+ stdio: 'inherit',
44
+ env,
45
+ });
46
+ return waitForClaude(child);
47
+ }
@@ -11,6 +11,7 @@ export interface CliCommandDependencies {
11
11
  isSidecarHealthy(): Promise<boolean>;
12
12
  launchCodex(args: readonly string[]): Promise<number>;
13
13
  launchCline(args: readonly string[], localToken: string): Promise<number>;
14
+ launchClaude(args: readonly string[], localToken: string): Promise<number>;
14
15
  }
15
16
  /** Handles the non-server CLI commands independently from terminal I/O. */
16
17
  export declare function runCliCommand(args: readonly string[], dependencies: CliCommandDependencies): Promise<number>;
@@ -57,7 +57,15 @@ export async function runCliCommand(args, dependencies) {
57
57
  const localToken = await dependencies.ensureSidecar();
58
58
  return dependencies.launchCline(args.slice(1), localToken);
59
59
  }
60
+ case 'claude': {
61
+ if (!await dependencies.isIdentityConfigured()) {
62
+ await dependencies.importMnemonic(await dependencies.readMnemonic());
63
+ dependencies.write('ZK Credits identity imported into the system credential store.');
64
+ }
65
+ const localToken = await dependencies.ensureSidecar();
66
+ return dependencies.launchClaude(args.slice(1), localToken);
67
+ }
60
68
  default:
61
- throw new Error('Usage: zk-credits <cline|setup codex|codex|status|token|import-mnemonic|serve|env>');
69
+ throw new Error('Usage: zk-credits <cline|claude|setup codex|codex|status|token|import-mnemonic|serve|env>');
62
70
  }
63
71
  }
package/dist/cli.js CHANGED
@@ -3,6 +3,7 @@ import { homedir } from 'node:os';
3
3
  import { join } from 'node:path';
4
4
  import { launchCodexProcess } from './codex-launcher.js';
5
5
  import { launchClineProcess } from './cline-launcher.js';
6
+ import { launchClaudeProcess } from './claude-launcher.js';
6
7
  import { isCodexProfileInstalled, resolveCodexHome, writeCodexProfile, } from './codex-profile.js';
7
8
  import { IdentityStore } from './identity.js';
8
9
  import { createLocalProofGenerator } from './local-prover.js';
@@ -79,6 +80,7 @@ async function readHiddenMnemonic() {
79
80
  function printHelp() {
80
81
  console.log(`Usage:
81
82
  zk-credits cline [cline arguments...]
83
+ zk-credits claude [claude arguments...]
82
84
  zk-credits setup codex [--model <model>]
83
85
  zk-credits codex [codex arguments...]
84
86
  zk-credits status
@@ -86,9 +88,9 @@ function printHelp() {
86
88
  zk-credits serve [--port <port>]
87
89
  eval "$(zk-credits env)"
88
90
 
89
- "zk-credits cline" is the zero-configuration coding-agent path: it starts the
90
- sidecar, configures an isolated OpenAI-compatible Cline profile, and forwards
91
- all Cline arguments. The Codex companion remains available through setup codex.
91
+ "zk-credits cline" and "zk-credits claude" configure and launch coding agents
92
+ against the proof-aware loopback sidecar. The Codex companion remains available
93
+ through setup codex.
92
94
  Set ZK_CREDITS_MNEMONIC only for a headless process; it is not persisted.`);
93
95
  }
94
96
  async function serve(args) {
@@ -173,6 +175,12 @@ async function main() {
173
175
  localToken,
174
176
  stateDirectory: sidecarHome,
175
177
  }),
178
+ launchClaude: (claudeArgs, localToken) => launchClaudeProcess({
179
+ args: claudeArgs,
180
+ loopbackBaseUrl: loopbackBaseUrl(),
181
+ localToken,
182
+ stateDirectory: sidecarHome,
183
+ }),
176
184
  });
177
185
  if (exitCode !== 0)
178
186
  process.exitCode = exitCode;
@@ -0,0 +1,13 @@
1
+ import type { CodexOptions, ThreadOptions } from '@openai/codex-sdk';
2
+ export interface BuildCodexSdkOptionsInput {
3
+ loopbackBaseUrl: string;
4
+ token: string;
5
+ codexHome: string;
6
+ env?: Record<string, string>;
7
+ }
8
+ export interface BuildCodexThreadOptionsInput {
9
+ model?: string;
10
+ workingDirectory?: string;
11
+ }
12
+ export declare function buildCodexSdkOptions(input: BuildCodexSdkOptionsInput): CodexOptions;
13
+ export declare function buildCodexThreadOptions(input?: BuildCodexThreadOptionsInput): ThreadOptions;
@@ -0,0 +1,33 @@
1
+ import { DEFAULT_CODEX_MODEL } from './codex-profile.js';
2
+ export function buildCodexSdkOptions(input) {
3
+ let url;
4
+ try {
5
+ url = new URL(input.loopbackBaseUrl);
6
+ }
7
+ catch {
8
+ throw new Error('Codex SDK provider must use a 127.0.0.1 HTTP sidecar URL with an explicit port');
9
+ }
10
+ if (url.protocol !== 'http:' || url.hostname !== '127.0.0.1' || !url.port) {
11
+ throw new Error('Codex SDK provider must use a 127.0.0.1 HTTP sidecar URL with an explicit port');
12
+ }
13
+ const baseUrl = `${url.origin}/v1`;
14
+ return {
15
+ baseUrl,
16
+ apiKey: input.token,
17
+ env: {
18
+ ...(input.env ?? process.env),
19
+ CODEX_HOME: input.codexHome,
20
+ OPENAI_BASE_URL: baseUrl,
21
+ OPENAI_API_KEY: input.token,
22
+ },
23
+ };
24
+ }
25
+ export function buildCodexThreadOptions(input) {
26
+ return {
27
+ model: input?.model ?? DEFAULT_CODEX_MODEL,
28
+ sandboxMode: 'read-only',
29
+ skipGitRepoCheck: true,
30
+ approvalPolicy: 'never',
31
+ ...(input?.workingDirectory ? { workingDirectory: input.workingDirectory } : {}),
32
+ };
33
+ }
package/dist/sidecar.js CHANGED
@@ -1,9 +1,21 @@
1
1
  import { createServer } from 'node:http';
2
+ import { anthropicErrorType, createAnthropicStreamTransformer, translateAnthropicToOpenAi, translateOpenAiToAnthropic, } from './anthropic-messages.js';
2
3
  import { codexModelsResponse } from './codex-profile.js';
3
4
  const MAX_REQUEST_BYTES = 2_000_000;
4
5
  function isLoopbackAddress(address) {
5
6
  return address === '127.0.0.1' || address === '::1' || address === '::ffff:127.0.0.1';
6
7
  }
8
+ function extractLocalToken(req) {
9
+ const authHeader = req.headers.authorization;
10
+ if (authHeader && authHeader.startsWith('Bearer ')) {
11
+ return authHeader.slice(7).trim();
12
+ }
13
+ const apiKeyHeader = req.headers['x-api-key'];
14
+ if (typeof apiKeyHeader === 'string') {
15
+ return apiKeyHeader.trim();
16
+ }
17
+ return null;
18
+ }
7
19
  function sendJson(res, status, body) {
8
20
  res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
9
21
  res.end(JSON.stringify(body));
@@ -71,19 +83,144 @@ export function createSidecarServer(options) {
71
83
  sendJson(res, 200, { service: 'zk-credits-sidecar', status: 'ok' });
72
84
  return;
73
85
  }
86
+ const token = extractLocalToken(req);
74
87
  if (req.method === 'GET' && pathname === '/v1/models') {
75
- if (req.headers.authorization !== `Bearer ${options.localToken}`) {
88
+ if (token !== options.localToken) {
76
89
  sendJson(res, 401, { error: 'invalid_local_token' });
77
90
  return;
78
91
  }
79
92
  sendJson(res, 200, codexModelsResponse());
80
93
  return;
81
94
  }
95
+ if (req.method === 'POST' && pathname === '/v1/messages') {
96
+ if (token !== options.localToken) {
97
+ sendJson(res, 401, { error: 'invalid_local_token' });
98
+ return;
99
+ }
100
+ try {
101
+ const body = await readJsonBody(req);
102
+ const anthropicReq = body.parsed;
103
+ const openAiReq = translateAnthropicToOpenAi(anthropicReq);
104
+ const rawOpenAiBody = JSON.stringify(openAiReq);
105
+ const reservation = await options.ledger.reserve(openAiReq);
106
+ const proof = await options.proofGenerator({
107
+ ticketIndex: reservation.index,
108
+ request: openAiReq,
109
+ });
110
+ const upstream = await gatewayFetch(`${gatewayBaseUrl}/v1/chat/completions`, {
111
+ method: 'POST',
112
+ headers: {
113
+ Authorization: `Bearer ${options.compatibilityKey}`,
114
+ 'Content-Type': 'application/json',
115
+ 'X-ZK-Proof': proofHeader(proof),
116
+ },
117
+ body: rawOpenAiBody,
118
+ });
119
+ if (upstream.status >= 200 && upstream.status < 300 && upstream.status !== 202) {
120
+ await options.ledger.consume(reservation.requestDigest);
121
+ }
122
+ if (!upstream.ok) {
123
+ const rawText = await upstream.text();
124
+ let errorMsg = rawText.trim() || `Gateway returned HTTP ${upstream.status}`;
125
+ try {
126
+ const errJson = JSON.parse(rawText);
127
+ if (errJson && typeof errJson === 'object') {
128
+ if ('error' in errJson && typeof errJson.error === 'string') {
129
+ errorMsg = errJson.error;
130
+ }
131
+ else if ('error' in errJson &&
132
+ errJson.error &&
133
+ typeof errJson.error === 'object' &&
134
+ 'message' in errJson.error &&
135
+ typeof errJson.error.message === 'string') {
136
+ errorMsg = errJson.error.message;
137
+ }
138
+ else if ('message' in errJson &&
139
+ typeof errJson.message === 'string') {
140
+ errorMsg = errJson.message;
141
+ }
142
+ }
143
+ }
144
+ catch {
145
+ // rawText is preserved as errorMsg
146
+ }
147
+ sendJson(res, upstream.status, {
148
+ type: 'error',
149
+ error: {
150
+ type: anthropicErrorType(upstream.status),
151
+ message: errorMsg,
152
+ },
153
+ });
154
+ return;
155
+ }
156
+ if (openAiReq.stream) {
157
+ res.writeHead(upstream.status, {
158
+ 'Content-Type': 'text/event-stream; charset=utf-8',
159
+ 'Cache-Control': 'no-cache',
160
+ Connection: 'keep-alive',
161
+ });
162
+ if (!upstream.body) {
163
+ res.end();
164
+ return;
165
+ }
166
+ const transformer = createAnthropicStreamTransformer({
167
+ model: openAiReq.model,
168
+ });
169
+ const reader = upstream.body.getReader();
170
+ const decoder = new TextDecoder();
171
+ let buffer = '';
172
+ while (true) {
173
+ const { done, value } = await reader.read();
174
+ if (done)
175
+ break;
176
+ buffer += decoder.decode(value, { stream: true });
177
+ const lines = buffer.split('\n');
178
+ buffer = lines.pop() ?? '';
179
+ for (const line of lines) {
180
+ const trimmed = line.trim();
181
+ if (trimmed.startsWith('data: ')) {
182
+ const dataStr = trimmed.slice(6);
183
+ if (dataStr === '[DONE]')
184
+ continue;
185
+ try {
186
+ const parsedChunk = JSON.parse(dataStr);
187
+ const sseEvents = transformer.transformChunk(parsedChunk);
188
+ for (const evt of sseEvents)
189
+ res.write(evt);
190
+ }
191
+ catch {
192
+ // ignore non-JSON chunk
193
+ }
194
+ }
195
+ }
196
+ }
197
+ const finishEvents = transformer.finish();
198
+ for (const evt of finishEvents)
199
+ res.write(evt);
200
+ res.end();
201
+ }
202
+ else {
203
+ const openAiRes = (await upstream.json());
204
+ const anthropicRes = translateOpenAiToAnthropic(openAiRes);
205
+ sendJson(res, upstream.status, anthropicRes);
206
+ }
207
+ }
208
+ catch (error) {
209
+ if (!res.headersSent) {
210
+ const message = error instanceof Error ? error.message : 'Loopback request failed';
211
+ sendJson(res, 502, { error: 'sidecar_request_failed', message });
212
+ }
213
+ else if (!res.writableEnded) {
214
+ res.end();
215
+ }
216
+ }
217
+ return;
218
+ }
82
219
  if (req.method !== 'POST' || (pathname !== '/v1/chat/completions' && pathname !== '/v1/responses')) {
83
220
  sendJson(res, 404, { error: 'unsupported_openai_path' });
84
221
  return;
85
222
  }
86
- if (req.headers.authorization !== `Bearer ${options.localToken}`) {
223
+ if (token !== options.localToken) {
87
224
  sendJson(res, 401, { error: 'invalid_local_token' });
88
225
  return;
89
226
  }
@@ -3,7 +3,7 @@ import { createRequire as __createNodeRequire } from 'node:module'; const requir
3
3
 
4
4
  // dist/cli.js
5
5
  import { homedir } from "node:os";
6
- import { join as join5 } from "node:path";
6
+ import { join as join6 } from "node:path";
7
7
 
8
8
  // dist/codex-launcher.js
9
9
  import { spawn } from "node:child_process";
@@ -221,6 +221,53 @@ async function launchClineProcess(options, spawnImpl = spawnCommand2) {
221
221
  }));
222
222
  }
223
223
 
224
+ // dist/claude-launcher.js
225
+ import { spawn as spawn3 } from "node:child_process";
226
+ import { chmod as chmod3, mkdir as mkdir3 } from "node:fs/promises";
227
+ import { join as join3 } from "node:path";
228
+ var spawnCommand3 = (command, args, options) => spawn3(command, [...args], options);
229
+ function anthropicBaseUrl(loopbackBaseUrl2) {
230
+ const url = new URL(loopbackBaseUrl2);
231
+ if (url.protocol !== "http:" || url.hostname !== "127.0.0.1") {
232
+ throw new Error("Claude Code companion must use a 127.0.0.1 HTTP sidecar URL");
233
+ }
234
+ return url.origin;
235
+ }
236
+ async function prepareClaudeDirectory(stateDirectory2) {
237
+ const configDirectory = join3(stateDirectory2, "claude");
238
+ await mkdir3(configDirectory, { recursive: true, mode: 448 });
239
+ await chmod3(configDirectory, 448);
240
+ return configDirectory;
241
+ }
242
+ function waitForClaude(child) {
243
+ return new Promise((resolve2, reject) => {
244
+ child.once("error", (error) => {
245
+ if (error.code === "ENOENT") {
246
+ reject(new Error("Claude Code CLI was not found; install it with npm install -g @anthropic-ai/claude-code before running zk-credits claude"));
247
+ return;
248
+ }
249
+ reject(error);
250
+ });
251
+ child.once("exit", (code) => resolve2(code ?? 1));
252
+ });
253
+ }
254
+ async function launchClaudeProcess(options, spawnImpl = spawnCommand3) {
255
+ const configDirectory = await prepareClaudeDirectory(options.stateDirectory);
256
+ const baseUrl = anthropicBaseUrl(options.loopbackBaseUrl);
257
+ const env = {
258
+ ...process.env,
259
+ CLAUDE_CONFIG_DIR: configDirectory,
260
+ ANTHROPIC_BASE_URL: baseUrl,
261
+ ANTHROPIC_AUTH_TOKEN: options.localToken,
262
+ DISABLE_TELEMETRY: "1"
263
+ };
264
+ const child = spawnImpl("claude", [...options.args], {
265
+ stdio: "inherit",
266
+ env
267
+ });
268
+ return waitForClaude(child);
269
+ }
270
+
224
271
  // dist/identity.js
225
272
  import * as keytar from "keytar";
226
273
 
@@ -492,7 +539,7 @@ import { resolve } from "node:path";
492
539
  // dist/artifact-manifest.js
493
540
  import { createHash } from "node:crypto";
494
541
  import { readFile } from "node:fs/promises";
495
- import { join as join3 } from "node:path";
542
+ import { join as join4 } from "node:path";
496
543
  var CIRCUIT_MANIFEST = [
497
544
  {
498
545
  file: "rln_nullifier.wasm",
@@ -509,7 +556,7 @@ var CIRCUIT_MANIFEST = [
509
556
  ];
510
557
  async function verifyArtifactManifest(artifactDirectory, manifest = CIRCUIT_MANIFEST) {
511
558
  for (const artifact of manifest) {
512
- const content = await readFile(join3(artifactDirectory, artifact.file));
559
+ const content = await readFile(join4(artifactDirectory, artifact.file));
513
560
  const digest = createHash("sha256").update(content).digest("hex");
514
561
  if (digest !== artifact.sha256) {
515
562
  throw new Error(`Circuit artifact hash mismatch: ${artifact.file}`);
@@ -603,15 +650,15 @@ var MembershipClient = class {
603
650
 
604
651
  // dist/sidecar-config.js
605
652
  import { randomBytes as randomBytes2 } from "node:crypto";
606
- import { join as join4 } from "node:path";
653
+ import { join as join5 } from "node:path";
607
654
  function createLoopbackToken() {
608
655
  return randomBytes2(32).toString("base64url");
609
656
  }
610
657
  function sidecarStatePaths(stateDirectory2) {
611
658
  return {
612
- ledgerPath: join4(stateDirectory2, "tickets.json"),
613
- tokenPath: join4(stateDirectory2, "loopback-token"),
614
- logPath: join4(stateDirectory2, "sidecar.log")
659
+ ledgerPath: join5(stateDirectory2, "tickets.json"),
660
+ tokenPath: join5(stateDirectory2, "loopback-token"),
661
+ logPath: join5(stateDirectory2, "sidecar.log")
615
662
  };
616
663
  }
617
664
  function formatOpenAiEnvironment(loopbackBaseUrl2, localToken) {
@@ -683,29 +730,37 @@ async function runCliCommand(args, dependencies) {
683
730
  const localToken = await dependencies.ensureSidecar();
684
731
  return dependencies.launchCline(args.slice(1), localToken);
685
732
  }
733
+ case "claude": {
734
+ if (!await dependencies.isIdentityConfigured()) {
735
+ await dependencies.importMnemonic(await dependencies.readMnemonic());
736
+ dependencies.write("ZK Credits identity imported into the system credential store.");
737
+ }
738
+ const localToken = await dependencies.ensureSidecar();
739
+ return dependencies.launchClaude(args.slice(1), localToken);
740
+ }
686
741
  default:
687
- throw new Error("Usage: zk-credits <cline|setup codex|codex|status|token|import-mnemonic|serve|env>");
742
+ throw new Error("Usage: zk-credits <cline|claude|setup codex|codex|status|token|import-mnemonic|serve|env>");
688
743
  }
689
744
  }
690
745
 
691
746
  // dist/node-sidecar-lifecycle.js
692
- import { spawn as spawn3 } from "node:child_process";
693
- import { chmod as chmod4, mkdir as mkdir4, open } from "node:fs/promises";
747
+ import { spawn as spawn4 } from "node:child_process";
748
+ import { chmod as chmod5, mkdir as mkdir5, open } from "node:fs/promises";
694
749
  import { dirname as dirname2 } from "node:path";
695
750
 
696
751
  // dist/sidecar-state.js
697
- import { chmod as chmod3, mkdir as mkdir3, readFile as readFile3, rename as rename2, writeFile as writeFile2 } from "node:fs/promises";
752
+ import { chmod as chmod4, mkdir as mkdir4, readFile as readFile3, rename as rename2, writeFile as writeFile2 } from "node:fs/promises";
698
753
  import { dirname } from "node:path";
699
754
  async function writeLoopbackToken(tokenPath, token) {
700
755
  if (!token)
701
756
  throw new Error("Loopback token is required");
702
- await mkdir3(dirname(tokenPath), { recursive: true, mode: 448 });
757
+ await mkdir4(dirname(tokenPath), { recursive: true, mode: 448 });
703
758
  const temporaryPath = `${tokenPath}.tmp`;
704
759
  await writeFile2(temporaryPath, `${token}
705
760
  `, { encoding: "utf8", mode: 384 });
706
- await chmod3(temporaryPath, 384);
761
+ await chmod4(temporaryPath, 384);
707
762
  await rename2(temporaryPath, tokenPath);
708
- await chmod3(tokenPath, 384);
763
+ await chmod4(tokenPath, 384);
709
764
  }
710
765
  async function readLoopbackToken(tokenPath) {
711
766
  const token = (await readFile3(tokenPath, "utf8")).trim();
@@ -716,11 +771,11 @@ async function readLoopbackToken(tokenPath) {
716
771
 
717
772
  // dist/node-sidecar-lifecycle.js
718
773
  async function startDetachedProcess(specification) {
719
- await mkdir4(dirname2(specification.logPath), { recursive: true, mode: 448 });
774
+ await mkdir5(dirname2(specification.logPath), { recursive: true, mode: 448 });
720
775
  const log = await open(specification.logPath, "a", 384);
721
- await chmod4(specification.logPath, 384);
776
+ await chmod5(specification.logPath, 384);
722
777
  try {
723
- const child = spawn3(specification.executable, specification.args, {
778
+ const child = spawn4(specification.executable, specification.args, {
724
779
  detached: true,
725
780
  env: specification.env,
726
781
  stdio: ["ignore", log.fd, log.fd]
@@ -798,10 +853,215 @@ async function ensureSidecarReady(dependencies, options = {}) {
798
853
 
799
854
  // dist/sidecar.js
800
855
  import { createServer } from "node:http";
856
+
857
+ // dist/anthropic-messages.js
858
+ function normalizeContent(content) {
859
+ if (!content)
860
+ return "";
861
+ if (typeof content === "string")
862
+ return content;
863
+ if (Array.isArray(content)) {
864
+ return content.map((block) => {
865
+ if (typeof block === "string")
866
+ return block;
867
+ if (block && typeof block.text === "string")
868
+ return block.text;
869
+ return "";
870
+ }).filter(Boolean).join("\n");
871
+ }
872
+ return "";
873
+ }
874
+ function resolveModel(requestedModel) {
875
+ if (!requestedModel)
876
+ return "openai/gpt-4o-mini";
877
+ if (requestedModel.includes("/"))
878
+ return requestedModel;
879
+ return "openai/gpt-4o-mini";
880
+ }
881
+ function mapFinishReason(reason) {
882
+ if (!reason || reason === "stop")
883
+ return "end_turn";
884
+ if (reason === "length")
885
+ return "max_tokens";
886
+ if (reason === "tool_calls" || reason === "function_call")
887
+ return "tool_use";
888
+ return "end_turn";
889
+ }
890
+ function anthropicErrorType(status) {
891
+ switch (status) {
892
+ case 400:
893
+ return "invalid_request_error";
894
+ case 401:
895
+ return "authentication_error";
896
+ case 403:
897
+ return "permission_error";
898
+ case 404:
899
+ return "not_found_error";
900
+ case 413:
901
+ return "request_too_large";
902
+ case 429:
903
+ return "rate_limit_error";
904
+ case 529:
905
+ return "overloaded_error";
906
+ default:
907
+ return status >= 500 ? "api_error" : "invalid_request_error";
908
+ }
909
+ }
910
+ function translateAnthropicToOpenAi(request) {
911
+ const messages = [];
912
+ const systemContent = normalizeContent(request.system);
913
+ if (systemContent) {
914
+ messages.push({ role: "system", content: systemContent });
915
+ }
916
+ for (const msg of request.messages || []) {
917
+ const content = normalizeContent(msg.content);
918
+ messages.push({
919
+ role: msg.role === "assistant" ? "assistant" : "user",
920
+ content
921
+ });
922
+ }
923
+ const openAiRequest = {
924
+ model: resolveModel(request.model),
925
+ messages
926
+ };
927
+ if (request.max_tokens !== void 0) {
928
+ openAiRequest.max_tokens = request.max_tokens;
929
+ }
930
+ if (request.temperature !== void 0) {
931
+ openAiRequest.temperature = request.temperature;
932
+ }
933
+ if (request.top_p !== void 0) {
934
+ openAiRequest.top_p = request.top_p;
935
+ }
936
+ if (request.stop_sequences && request.stop_sequences.length > 0) {
937
+ openAiRequest.stop = request.stop_sequences;
938
+ }
939
+ if (request.stream) {
940
+ openAiRequest.stream = true;
941
+ }
942
+ return openAiRequest;
943
+ }
944
+ function translateOpenAiToAnthropic(response) {
945
+ const choice = response.choices?.[0];
946
+ const text = choice?.message?.content ?? "";
947
+ const stopReason = mapFinishReason(choice?.finish_reason);
948
+ return {
949
+ id: response.id?.startsWith("msg_") ? response.id : `msg_${response.id || "zk_" + Date.now()}`,
950
+ type: "message",
951
+ role: "assistant",
952
+ model: response.model || "openai/gpt-4o-mini",
953
+ content: [{ type: "text", text }],
954
+ stop_reason: stopReason,
955
+ stop_sequence: null,
956
+ usage: {
957
+ input_tokens: response.usage?.prompt_tokens ?? 0,
958
+ output_tokens: response.usage?.completion_tokens ?? 0
959
+ }
960
+ };
961
+ }
962
+ function createAnthropicStreamTransformer(options) {
963
+ const messageId = options.id || `msg_zk_${Date.now()}`;
964
+ const model = options.model;
965
+ let started = false;
966
+ let blockStarted = false;
967
+ let blockIndex = 0;
968
+ let promptTokens = 0;
969
+ let completionTokens = 0;
970
+ let finalStopReason = null;
971
+ function sse(event, data) {
972
+ return `event: ${event}
973
+ data: ${JSON.stringify(data)}
974
+
975
+ `;
976
+ }
977
+ return {
978
+ transformChunk(rawChunk) {
979
+ const chunk = rawChunk;
980
+ const out = [];
981
+ if (!started) {
982
+ started = true;
983
+ out.push(sse("message_start", {
984
+ type: "message_start",
985
+ message: {
986
+ id: messageId,
987
+ type: "message",
988
+ role: "assistant",
989
+ model,
990
+ content: [],
991
+ stop_reason: null,
992
+ stop_sequence: null,
993
+ usage: { input_tokens: 0, output_tokens: 0 }
994
+ }
995
+ }));
996
+ }
997
+ if (chunk.usage) {
998
+ if (chunk.usage.prompt_tokens)
999
+ promptTokens = chunk.usage.prompt_tokens;
1000
+ if (chunk.usage.completion_tokens)
1001
+ completionTokens = chunk.usage.completion_tokens;
1002
+ }
1003
+ const choice = chunk.choices?.[0];
1004
+ const deltaContent = choice?.delta?.content;
1005
+ if (deltaContent) {
1006
+ if (!blockStarted) {
1007
+ blockStarted = true;
1008
+ out.push(sse("content_block_start", {
1009
+ type: "content_block_start",
1010
+ index: blockIndex,
1011
+ content_block: { type: "text", text: "" }
1012
+ }));
1013
+ }
1014
+ out.push(sse("content_block_delta", {
1015
+ type: "content_block_delta",
1016
+ index: blockIndex,
1017
+ delta: { type: "text_delta", text: deltaContent }
1018
+ }));
1019
+ }
1020
+ if (choice?.finish_reason) {
1021
+ finalStopReason = mapFinishReason(choice.finish_reason);
1022
+ }
1023
+ return out;
1024
+ },
1025
+ finish() {
1026
+ const out = [];
1027
+ if (blockStarted) {
1028
+ out.push(sse("content_block_stop", {
1029
+ type: "content_block_stop",
1030
+ index: blockIndex
1031
+ }));
1032
+ }
1033
+ out.push(sse("message_delta", {
1034
+ type: "message_delta",
1035
+ delta: {
1036
+ stop_reason: finalStopReason || "end_turn",
1037
+ stop_sequence: null
1038
+ },
1039
+ usage: { output_tokens: completionTokens }
1040
+ }));
1041
+ out.push(sse("message_stop", {
1042
+ type: "message_stop"
1043
+ }));
1044
+ return out;
1045
+ }
1046
+ };
1047
+ }
1048
+
1049
+ // dist/sidecar.js
801
1050
  var MAX_REQUEST_BYTES = 2e6;
802
1051
  function isLoopbackAddress(address) {
803
1052
  return address === "127.0.0.1" || address === "::1" || address === "::ffff:127.0.0.1";
804
1053
  }
1054
+ function extractLocalToken(req) {
1055
+ const authHeader = req.headers.authorization;
1056
+ if (authHeader && authHeader.startsWith("Bearer ")) {
1057
+ return authHeader.slice(7).trim();
1058
+ }
1059
+ const apiKeyHeader = req.headers["x-api-key"];
1060
+ if (typeof apiKeyHeader === "string") {
1061
+ return apiKeyHeader.trim();
1062
+ }
1063
+ return null;
1064
+ }
805
1065
  function sendJson(res, status, body) {
806
1066
  res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
807
1067
  res.end(JSON.stringify(body));
@@ -863,19 +1123,130 @@ function createSidecarServer(options) {
863
1123
  sendJson(res, 200, { service: "zk-credits-sidecar", status: "ok" });
864
1124
  return;
865
1125
  }
1126
+ const token = extractLocalToken(req);
866
1127
  if (req.method === "GET" && pathname === "/v1/models") {
867
- if (req.headers.authorization !== `Bearer ${options.localToken}`) {
1128
+ if (token !== options.localToken) {
868
1129
  sendJson(res, 401, { error: "invalid_local_token" });
869
1130
  return;
870
1131
  }
871
1132
  sendJson(res, 200, codexModelsResponse());
872
1133
  return;
873
1134
  }
1135
+ if (req.method === "POST" && pathname === "/v1/messages") {
1136
+ if (token !== options.localToken) {
1137
+ sendJson(res, 401, { error: "invalid_local_token" });
1138
+ return;
1139
+ }
1140
+ try {
1141
+ const body = await readJsonBody(req);
1142
+ const anthropicReq = body.parsed;
1143
+ const openAiReq = translateAnthropicToOpenAi(anthropicReq);
1144
+ const rawOpenAiBody = JSON.stringify(openAiReq);
1145
+ const reservation = await options.ledger.reserve(openAiReq);
1146
+ const proof = await options.proofGenerator({
1147
+ ticketIndex: reservation.index,
1148
+ request: openAiReq
1149
+ });
1150
+ const upstream = await gatewayFetch(`${gatewayBaseUrl}/v1/chat/completions`, {
1151
+ method: "POST",
1152
+ headers: {
1153
+ Authorization: `Bearer ${options.compatibilityKey}`,
1154
+ "Content-Type": "application/json",
1155
+ "X-ZK-Proof": proofHeader(proof)
1156
+ },
1157
+ body: rawOpenAiBody
1158
+ });
1159
+ if (upstream.status >= 200 && upstream.status < 300 && upstream.status !== 202) {
1160
+ await options.ledger.consume(reservation.requestDigest);
1161
+ }
1162
+ if (!upstream.ok) {
1163
+ const rawText = await upstream.text();
1164
+ let errorMsg = rawText.trim() || `Gateway returned HTTP ${upstream.status}`;
1165
+ try {
1166
+ const errJson = JSON.parse(rawText);
1167
+ if (errJson && typeof errJson === "object") {
1168
+ if ("error" in errJson && typeof errJson.error === "string") {
1169
+ errorMsg = errJson.error;
1170
+ } else if ("error" in errJson && errJson.error && typeof errJson.error === "object" && "message" in errJson.error && typeof errJson.error.message === "string") {
1171
+ errorMsg = errJson.error.message;
1172
+ } else if ("message" in errJson && typeof errJson.message === "string") {
1173
+ errorMsg = errJson.message;
1174
+ }
1175
+ }
1176
+ } catch {
1177
+ }
1178
+ sendJson(res, upstream.status, {
1179
+ type: "error",
1180
+ error: {
1181
+ type: anthropicErrorType(upstream.status),
1182
+ message: errorMsg
1183
+ }
1184
+ });
1185
+ return;
1186
+ }
1187
+ if (openAiReq.stream) {
1188
+ res.writeHead(upstream.status, {
1189
+ "Content-Type": "text/event-stream; charset=utf-8",
1190
+ "Cache-Control": "no-cache",
1191
+ Connection: "keep-alive"
1192
+ });
1193
+ if (!upstream.body) {
1194
+ res.end();
1195
+ return;
1196
+ }
1197
+ const transformer = createAnthropicStreamTransformer({
1198
+ model: openAiReq.model
1199
+ });
1200
+ const reader = upstream.body.getReader();
1201
+ const decoder = new TextDecoder();
1202
+ let buffer = "";
1203
+ while (true) {
1204
+ const { done, value } = await reader.read();
1205
+ if (done)
1206
+ break;
1207
+ buffer += decoder.decode(value, { stream: true });
1208
+ const lines = buffer.split("\n");
1209
+ buffer = lines.pop() ?? "";
1210
+ for (const line of lines) {
1211
+ const trimmed = line.trim();
1212
+ if (trimmed.startsWith("data: ")) {
1213
+ const dataStr = trimmed.slice(6);
1214
+ if (dataStr === "[DONE]")
1215
+ continue;
1216
+ try {
1217
+ const parsedChunk = JSON.parse(dataStr);
1218
+ const sseEvents = transformer.transformChunk(parsedChunk);
1219
+ for (const evt of sseEvents)
1220
+ res.write(evt);
1221
+ } catch {
1222
+ }
1223
+ }
1224
+ }
1225
+ }
1226
+ const finishEvents = transformer.finish();
1227
+ for (const evt of finishEvents)
1228
+ res.write(evt);
1229
+ res.end();
1230
+ } else {
1231
+ const openAiRes = await upstream.json();
1232
+ const anthropicRes = translateOpenAiToAnthropic(openAiRes);
1233
+ sendJson(res, upstream.status, anthropicRes);
1234
+ }
1235
+ } catch (error) {
1236
+ if (!res.headersSent) {
1237
+ const message = error instanceof Error ? error.message : "Loopback request failed";
1238
+ sendJson(res, 502, { error: "sidecar_request_failed", message });
1239
+ } else if (!res.writableEnded) {
1240
+ res.end();
1241
+ }
1242
+ }
1243
+ return;
1244
+ }
874
1245
  if (req.method !== "POST" || pathname !== "/v1/chat/completions" && pathname !== "/v1/responses") {
875
1246
  sendJson(res, 404, { error: "unsupported_openai_path" });
876
1247
  return;
877
1248
  }
878
- if (req.headers.authorization !== `Bearer ${options.localToken}`) {
1249
+ if (token !== options.localToken) {
879
1250
  sendJson(res, 401, { error: "invalid_local_token" });
880
1251
  return;
881
1252
  }
@@ -943,7 +1314,7 @@ function createSidecarServer(options) {
943
1314
  }
944
1315
 
945
1316
  // dist/ticket-ledger.js
946
- import { mkdir as mkdir5, readFile as readFile4, rename as rename3, writeFile as writeFile3 } from "node:fs/promises";
1317
+ import { mkdir as mkdir6, readFile as readFile4, rename as rename3, writeFile as writeFile3 } from "node:fs/promises";
947
1318
  import { dirname as dirname3 } from "node:path";
948
1319
  function parseLedger(raw) {
949
1320
  const parsed = JSON.parse(raw);
@@ -1016,7 +1387,7 @@ var TicketLedger = class {
1016
1387
  }
1017
1388
  }
1018
1389
  async write(ledger) {
1019
- await mkdir5(dirname3(this.filePath), { recursive: true, mode: 448 });
1390
+ await mkdir6(dirname3(this.filePath), { recursive: true, mode: 448 });
1020
1391
  const temporaryPath = `${this.filePath}.tmp`;
1021
1392
  await writeFile3(temporaryPath, JSON.stringify(ledger), { encoding: "utf8", mode: 384 });
1022
1393
  await rename3(temporaryPath, this.filePath);
@@ -1027,7 +1398,7 @@ var TicketLedger = class {
1027
1398
  var DEFAULT_GATEWAY_URL = "https://zk-credits-gateway.onrender.com";
1028
1399
  var DEFAULT_PORT = 3210;
1029
1400
  function stateDirectory() {
1030
- return process.env.ZK_CREDITS_HOME || join5(homedir(), ".zk-credits");
1401
+ return process.env.ZK_CREDITS_HOME || join6(homedir(), ".zk-credits");
1031
1402
  }
1032
1403
  function loopbackBaseUrl() {
1033
1404
  const port = Number(process.env.ZK_CREDITS_SIDECAR_PORT || DEFAULT_PORT);
@@ -1087,6 +1458,7 @@ async function readHiddenMnemonic() {
1087
1458
  function printHelp() {
1088
1459
  console.log(`Usage:
1089
1460
  zk-credits cline [cline arguments...]
1461
+ zk-credits claude [claude arguments...]
1090
1462
  zk-credits setup codex [--model <model>]
1091
1463
  zk-credits codex [codex arguments...]
1092
1464
  zk-credits status
@@ -1094,9 +1466,9 @@ function printHelp() {
1094
1466
  zk-credits serve [--port <port>]
1095
1467
  eval "$(zk-credits env)"
1096
1468
 
1097
- "zk-credits cline" is the zero-configuration coding-agent path: it starts the
1098
- sidecar, configures an isolated OpenAI-compatible Cline profile, and forwards
1099
- all Cline arguments. The Codex companion remains available through setup codex.
1469
+ "zk-credits cline" and "zk-credits claude" configure and launch coding agents
1470
+ against the proof-aware loopback sidecar. The Codex companion remains available
1471
+ through setup codex.
1100
1472
  Set ZK_CREDITS_MNEMONIC only for a headless process; it is not persisted.`);
1101
1473
  }
1102
1474
  async function serve(args) {
@@ -1185,6 +1557,12 @@ async function main() {
1185
1557
  loopbackBaseUrl: loopbackBaseUrl(),
1186
1558
  localToken,
1187
1559
  stateDirectory: sidecarHome
1560
+ }),
1561
+ launchClaude: (claudeArgs, localToken) => launchClaudeProcess({
1562
+ args: claudeArgs,
1563
+ loopbackBaseUrl: loopbackBaseUrl(),
1564
+ localToken,
1565
+ stateDirectory: sidecarHome
1188
1566
  })
1189
1567
  });
1190
1568
  if (exitCode !== 0)
package/package.json CHANGED
@@ -1,7 +1,14 @@
1
1
  {
2
2
  "name": "zk-credits",
3
- "version": "0.1.1",
4
- "description": "Loopback proof-aware OpenAI transport for ZK Credits.",
3
+ "version": "0.1.3",
4
+ "description": "Loopback proof-aware OpenAI transport for Cline, Claude Code, and Codex.",
5
+ "homepage": "https://feature-zk-api-credits-gadillacers-projects.vercel.app",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/mangekyou-labs/haze-api.git",
9
+ "directory": "packages/zk-credits-sidecar"
10
+ },
11
+ "license": "MIT",
5
12
  "type": "module",
6
13
  "engines": {
7
14
  "node": ">=20"
@@ -9,6 +16,12 @@
9
16
  "bin": {
10
17
  "zk-credits": "dist/zk-credits.js"
11
18
  },
19
+ "exports": {
20
+ "./codex": {
21
+ "types": "./dist/codex-sdk-options.d.ts",
22
+ "import": "./dist/codex-sdk-options.js"
23
+ }
24
+ },
12
25
  "files": [
13
26
  "dist",
14
27
  "circuits"
@@ -27,6 +40,7 @@
27
40
  "snarkjs": "^0.7.6"
28
41
  },
29
42
  "devDependencies": {
43
+ "@openai/codex-sdk": "^0.150.1",
30
44
  "@types/node": "^24.13.3",
31
45
  "@zk-credits/shared": "file:../zk-credits-shared",
32
46
  "esbuild": "^0.28.2",