zk-credits 0.1.0 → 0.1.2

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.
@@ -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
+ }
@@ -10,6 +10,8 @@ export interface CliCommandDependencies {
10
10
  isCodexProfileInstalled(): Promise<boolean>;
11
11
  isSidecarHealthy(): Promise<boolean>;
12
12
  launchCodex(args: readonly string[]): Promise<number>;
13
+ launchCline(args: readonly string[], localToken: string): Promise<number>;
14
+ launchClaude(args: readonly string[], localToken: string): Promise<number>;
13
15
  }
14
16
  /** Handles the non-server CLI commands independently from terminal I/O. */
15
17
  export declare function runCliCommand(args: readonly string[], dependencies: CliCommandDependencies): Promise<number>;
@@ -49,7 +49,23 @@ export async function runCliCommand(args, dependencies) {
49
49
  await dependencies.ensureSidecar();
50
50
  return dependencies.launchCodex(args.slice(1));
51
51
  }
52
+ case 'cline': {
53
+ if (!await dependencies.isIdentityConfigured()) {
54
+ await dependencies.importMnemonic(await dependencies.readMnemonic());
55
+ dependencies.write('ZK Credits identity imported into the system credential store.');
56
+ }
57
+ const localToken = await dependencies.ensureSidecar();
58
+ return dependencies.launchCline(args.slice(1), localToken);
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
+ }
52
68
  default:
53
- throw new Error('Usage: zk-credits <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>');
54
70
  }
55
71
  }
package/dist/cli.js CHANGED
@@ -2,6 +2,8 @@
2
2
  import { homedir } from 'node:os';
3
3
  import { join } from 'node:path';
4
4
  import { launchCodexProcess } from './codex-launcher.js';
5
+ import { launchClineProcess } from './cline-launcher.js';
6
+ import { launchClaudeProcess } from './claude-launcher.js';
5
7
  import { isCodexProfileInstalled, resolveCodexHome, writeCodexProfile, } from './codex-profile.js';
6
8
  import { IdentityStore } from './identity.js';
7
9
  import { createLocalProofGenerator } from './local-prover.js';
@@ -77,6 +79,8 @@ async function readHiddenMnemonic() {
77
79
  }
78
80
  function printHelp() {
79
81
  console.log(`Usage:
82
+ zk-credits cline [cline arguments...]
83
+ zk-credits claude [claude arguments...]
80
84
  zk-credits setup codex [--model <model>]
81
85
  zk-credits codex [codex arguments...]
82
86
  zk-credits status
@@ -84,10 +88,10 @@ function printHelp() {
84
88
  zk-credits serve [--port <port>]
85
89
  eval "$(zk-credits env)"
86
90
 
87
- The Codex setup starts the loopback sidecar automatically; daily use is
88
- "zk-credits codex". serve/env remain available for other OpenAI-compatible
89
- clients. Set ZK_CREDITS_MNEMONIC only for a headless process; it is not
90
- persisted by that path.`);
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.
94
+ Set ZK_CREDITS_MNEMONIC only for a headless process; it is not persisted.`);
91
95
  }
92
96
  async function serve(args) {
93
97
  const port = readPort(args);
@@ -165,6 +169,18 @@ async function main() {
165
169
  isCodexProfileInstalled: () => isCodexProfileInstalled(codexHome),
166
170
  isSidecarHealthy: () => lifecycle.isHealthy(),
167
171
  launchCodex: (codexArgs) => launchCodexProcess(codexArgs),
172
+ launchCline: (clineArgs, localToken) => launchClineProcess({
173
+ args: clineArgs,
174
+ loopbackBaseUrl: loopbackBaseUrl(),
175
+ localToken,
176
+ stateDirectory: sidecarHome,
177
+ }),
178
+ launchClaude: (claudeArgs, localToken) => launchClaudeProcess({
179
+ args: claudeArgs,
180
+ loopbackBaseUrl: loopbackBaseUrl(),
181
+ localToken,
182
+ stateDirectory: sidecarHome,
183
+ }),
168
184
  });
169
185
  if (exitCode !== 0)
170
186
  process.exitCode = exitCode;
@@ -0,0 +1,18 @@
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
+ type ClineStdio = 'inherit' | ['ignore', 'ignore', 'inherit'];
6
+ export type ClineSpawnCommand = (command: string, args: readonly string[], options: {
7
+ stdio: ClineStdio;
8
+ env: NodeJS.ProcessEnv;
9
+ }) => SpawnedProcess;
10
+ export interface LaunchClineOptions {
11
+ args: readonly string[];
12
+ loopbackBaseUrl: string;
13
+ localToken: string;
14
+ stateDirectory: string;
15
+ }
16
+ /** Configures and launches Cline against the proof-aware loopback transport. */
17
+ export declare function launchClineProcess(options: LaunchClineOptions, spawnImpl?: ClineSpawnCommand): Promise<number>;
18
+ export {};
@@ -0,0 +1,85 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { chmod, mkdir } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ const DEFAULT_CLINE_MODEL = 'openai/gpt-4o-mini';
5
+ const spawnCommand = (command, args, options) => spawn(command, [...args], options);
6
+ function openAiBaseUrl(loopbackBaseUrl) {
7
+ const url = new URL(loopbackBaseUrl);
8
+ if (url.protocol !== 'http:' || url.hostname !== '127.0.0.1') {
9
+ throw new Error('Cline companion must use a 127.0.0.1 HTTP sidecar URL');
10
+ }
11
+ return `${url.origin}/v1`;
12
+ }
13
+ function selectedModel(args) {
14
+ for (let index = 0; index < args.length; index += 1) {
15
+ const argument = args[index];
16
+ if (argument === '--model' || argument === '-m') {
17
+ const model = args[index + 1]?.trim();
18
+ if (!model)
19
+ throw new Error(`${argument} requires a model ID`);
20
+ return model;
21
+ }
22
+ if (argument?.startsWith('--model=')) {
23
+ const model = argument.slice('--model='.length).trim();
24
+ if (!model)
25
+ throw new Error('--model requires a model ID');
26
+ return model;
27
+ }
28
+ }
29
+ return DEFAULT_CLINE_MODEL;
30
+ }
31
+ function assertManagedProviderArguments(args) {
32
+ const managed = new Set(['--provider', '-P', '--key', '-k', '--config', '--data-dir']);
33
+ if (args.some((argument) => managed.has(argument) || argument.startsWith('--provider='))) {
34
+ throw new Error('zk-credits cline manages the provider, API key, and profile directory');
35
+ }
36
+ }
37
+ function waitForCline(child) {
38
+ return new Promise((resolve, reject) => {
39
+ child.once('error', (error) => {
40
+ if (error.code === 'ENOENT') {
41
+ reject(new Error('Cline CLI was not found; install it with npm install --global cline'));
42
+ return;
43
+ }
44
+ reject(error);
45
+ });
46
+ child.once('exit', (code) => resolve(code ?? 1));
47
+ });
48
+ }
49
+ /** Configures and launches Cline against the proof-aware loopback transport. */
50
+ export async function launchClineProcess(options, spawnImpl = spawnCommand) {
51
+ assertManagedProviderArguments(options.args);
52
+ const baseUrl = openAiBaseUrl(options.loopbackBaseUrl);
53
+ const model = selectedModel(options.args);
54
+ const dataDirectory = join(options.stateDirectory, 'cline');
55
+ await mkdir(dataDirectory, { recursive: true, mode: 0o700 });
56
+ await chmod(dataDirectory, 0o700);
57
+ const environment = {
58
+ ...process.env,
59
+ OPENAI_BASE_URL: baseUrl,
60
+ OPENAI_API_KEY: options.localToken,
61
+ };
62
+ const authCode = await waitForCline(spawnImpl('cline', [
63
+ 'auth',
64
+ '--provider', 'openai-compatible',
65
+ '--apikey', options.localToken,
66
+ '--modelid', model,
67
+ '--baseurl', baseUrl,
68
+ '--data-dir', dataDirectory,
69
+ ], {
70
+ stdio: ['ignore', 'ignore', 'inherit'],
71
+ env: environment,
72
+ }));
73
+ if (authCode !== 0) {
74
+ throw new Error(`Cline OpenAI-compatible provider setup failed with exit code ${authCode}`);
75
+ }
76
+ return waitForCline(spawnImpl('cline', [
77
+ '--provider', 'openai-compatible',
78
+ '--model', model,
79
+ '--data-dir', dataDirectory,
80
+ ...options.args,
81
+ ], {
82
+ stdio: 'inherit',
83
+ env: environment,
84
+ }));
85
+ }
@@ -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
+ }