sleev 0.0.10 → 0.0.12

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.
@@ -1,8 +1,12 @@
1
1
  // Claude Code setup owns global settings patching for the Anthropic-compatible proxy.
2
- import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises';
3
3
  import { homedir } from 'node:os';
4
4
  import { dirname, join } from 'node:path';
5
5
  import { field, success } from '../terminal.js';
6
+ const BASE = 'ANTHROPIC_BASE_URL';
7
+ const CUSTOM = 'ANTHROPIC_CUSTOM_HEADERS';
8
+ const HARNESS = 'claude code';
9
+ const OLD_HARNESS = 'claude-code';
6
10
  export async function setupClaude(auth, path = claudePath()) {
7
11
  const result = await patchClaude(auth, path);
8
12
  success(process.stdout, 'Configured Claude Code');
@@ -12,8 +16,7 @@ export async function setupClaude(auth, path = claudePath()) {
12
16
  export async function patchClaude(auth, path = claudePath()) {
13
17
  const existing = await readJson(path);
14
18
  const config = patchConfig(existing, auth);
15
- await mkdir(dirname(path), { recursive: true, mode: 0o700 });
16
- await writeFile(path, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
19
+ await writeJson(path, config);
17
20
  return { path };
18
21
  }
19
22
  export async function unpatchClaude(path = claudePath()) {
@@ -23,23 +26,22 @@ export async function unpatchClaude(path = claudePath()) {
23
26
  const env = object(existing.env);
24
27
  if (typeof env.ANTHROPIC_CUSTOM_HEADERS !== 'string')
25
28
  return;
26
- if (!env.ANTHROPIC_CUSTOM_HEADERS.includes('sleeve-harness: claude-code'))
29
+ if (!patched(env))
27
30
  return;
28
- const { ANTHROPIC_BASE_URL: _base, ANTHROPIC_CUSTOM_HEADERS: _headers, ...rest } = env;
29
- await mkdir(dirname(path), { recursive: true, mode: 0o700 });
30
- await writeFile(path, `${JSON.stringify({ ...existing, env: rest }, null, 2)}\n`, { mode: 0o600 });
31
+ await writeJson(path, removeBackup({ ...existing, env: restoreEnv(env, backup(existing)) }));
31
32
  }
32
33
  export function claudePath(env = process.env) {
33
34
  return join(env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude'), 'settings.json');
34
35
  }
35
36
  function patchConfig(input, auth) {
36
37
  const env = object(input.env);
38
+ const previous = backup(input) ?? backupEnv(env);
37
39
  return {
38
- ...input,
40
+ ...withBackup(input, previous),
39
41
  env: {
40
42
  ...env,
41
- ANTHROPIC_BASE_URL: trim(auth.proxyUrl),
42
- ANTHROPIC_CUSTOM_HEADERS: headers(auth),
43
+ [BASE]: trim(auth.proxyUrl),
44
+ [CUSTOM]: headers(auth),
43
45
  },
44
46
  };
45
47
  }
@@ -47,9 +49,68 @@ function headers(auth) {
47
49
  return [
48
50
  `sleeve-token: ${auth.token}`,
49
51
  'sleeve-provider: anthropic',
50
- 'sleeve-harness: claude-code',
52
+ `sleeve-harness: ${HARNESS}`,
51
53
  ].join('\n');
52
54
  }
55
+ function backup(input) {
56
+ const sleev = object(input.sleev);
57
+ const claude = object(sleev.claudeCode);
58
+ if (!('previousEnv' in claude))
59
+ return null;
60
+ return object(claude.previousEnv);
61
+ }
62
+ function backupEnv(env) {
63
+ if (patched(env))
64
+ return {};
65
+ const previous = {};
66
+ if (BASE in env)
67
+ previous[BASE] = env[BASE];
68
+ if (CUSTOM in env)
69
+ previous[CUSTOM] = env[CUSTOM];
70
+ return previous;
71
+ }
72
+ function withBackup(input, previous) {
73
+ const sleev = object(input.sleev);
74
+ return {
75
+ ...input,
76
+ sleev: {
77
+ ...sleev,
78
+ claudeCode: {
79
+ previousEnv: previous,
80
+ },
81
+ },
82
+ };
83
+ }
84
+ function removeBackup(input) {
85
+ const sleev = object(input.sleev);
86
+ const { claudeCode: _claude, ...rest } = sleev;
87
+ if (Object.keys(rest).length > 0)
88
+ return { ...input, sleev: rest };
89
+ const { sleev: _sleev, ...config } = input;
90
+ return config;
91
+ }
92
+ function restoreEnv(env, previous) {
93
+ const { [BASE]: _base, [CUSTOM]: _headers, ...rest } = env;
94
+ const restored = { ...rest };
95
+ if (!previous)
96
+ return restored;
97
+ if (BASE in previous)
98
+ restored[BASE] = previous[BASE];
99
+ if (CUSTOM in previous)
100
+ restored[CUSTOM] = previous[CUSTOM];
101
+ return restored;
102
+ }
103
+ function patched(env) {
104
+ const value = env[CUSTOM];
105
+ if (typeof value !== 'string')
106
+ return false;
107
+ return value.includes(`sleeve-harness: ${HARNESS}`) || value.includes(`sleeve-harness: ${OLD_HARNESS}`);
108
+ }
109
+ async function writeJson(path, config) {
110
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
111
+ await writeFile(path, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
112
+ await chmod(path, 0o600);
113
+ }
53
114
  async function readJson(path) {
54
115
  try {
55
116
  const raw = await readFile(path, 'utf8');
@@ -1,8 +1,11 @@
1
1
  // Codex setup owns global TOML patching for the OpenAI-compatible proxy provider.
2
- import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises';
3
3
  import { homedir } from 'node:os';
4
4
  import { dirname, join } from 'node:path';
5
5
  import { field, success } from '../terminal.js';
6
+ const PROVIDER = 'sleev';
7
+ const HARNESS = 'codex cli';
8
+ const OLD_HARNESS = 'codex';
6
9
  export async function setupCodex(auth, path = codexPath()) {
7
10
  const result = await patchCodex(auth, path);
8
11
  success(process.stdout, 'Configured Codex');
@@ -12,8 +15,7 @@ export async function setupCodex(auth, path = codexPath()) {
12
15
  export async function patchCodex(auth, path = codexPath()) {
13
16
  const existing = await readText(path);
14
17
  const config = patchCodexConfig(existing, auth);
15
- await mkdir(dirname(path), { recursive: true, mode: 0o700 });
16
- await writeFile(path, config, { mode: 0o600 });
18
+ await writeText(path, config);
17
19
  return { path };
18
20
  }
19
21
  export async function unpatchCodex(path = codexPath()) {
@@ -21,25 +23,25 @@ export async function unpatchCodex(path = codexPath()) {
21
23
  if (!existing)
22
24
  return;
23
25
  const config = unpatchCodexConfig(existing);
24
- await mkdir(dirname(path), { recursive: true, mode: 0o700 });
25
- await writeFile(path, config, { mode: 0o600 });
26
+ await writeText(path, config);
26
27
  }
27
28
  export function codexPath(env = process.env) {
28
29
  return join(env.CODEX_HOME || join(homedir(), '.codex'), 'config.toml');
29
30
  }
30
31
  export function patchCodexConfig(input, auth) {
31
- const provider = rootString(input, 'model_provider') ?? 'openai';
32
- const target = reserved(provider) ? 'sleev' : provider;
33
- const stale = removeTable(input.trimEnd(), 'model_providers.sleev');
34
- const clean = target === provider ? stale : removeRootKey(stale, 'model_provider');
35
- const root = target === provider ? clean : insertRootKey(clean.trimEnd(), 'model_provider', target);
36
- const base = trim(auth.proxyUrl);
37
- return `${patchProvider(root, target, auth, base).trimEnd()}\n`;
32
+ const previous = previousProvider(input);
33
+ const stale = removeTable(removeTable(input.trimEnd(), table(PROVIDER)), 'sleev.codex');
34
+ const clean = removeRootKey(stale, 'model_provider');
35
+ const root = insertRootKey(clean.trimEnd(), 'model_provider', PROVIDER);
36
+ const meta = previous ? `\n\n[sleev.codex]\nprevious_model_provider = "${toml(previous)}"` : '';
37
+ return `${patchProvider(root, auth).trimEnd()}${meta}\n`;
38
38
  }
39
39
  export function unpatchCodexConfig(input) {
40
+ const previous = backup(input);
40
41
  const provider = rootString(input, 'model_provider') ?? 'openai';
41
- const clean = removeTable(removeRootKey(input.trimEnd(), 'model_provider'), 'model_providers.sleev');
42
- const config = provider === 'sleev' ? clean : removeSleevKeys(input, provider);
42
+ const clean = removeTable(removeTable(removeRootKey(input.trimEnd(), 'model_provider'), table(PROVIDER)), 'sleev.codex');
43
+ const restored = previous ? insertRootKey(clean.trimEnd(), 'model_provider', previous) : clean;
44
+ const config = provider === PROVIDER ? restored : removeSleevKeys(input, provider);
43
45
  return `${config.trimEnd()}\n`;
44
46
  }
45
47
  function insertRootKey(input, key, value) {
@@ -73,11 +75,11 @@ function removeRootKey(input, key) {
73
75
  }
74
76
  return next.join('\n');
75
77
  }
76
- function removeTable(input, table) {
78
+ function removeTable(input, name) {
77
79
  const lines = input.split('\n');
78
80
  const next = [];
79
81
  let skip = false;
80
- const header = new RegExp(`^\\s*\\[\\s*${escape(table)}(?:\\.|\\s*\\])`);
82
+ const header = new RegExp(`^\\s*\\[\\s*${escape(name)}(?:\\.|\\s*\\])`);
81
83
  for (const line of lines) {
82
84
  if (header.test(line)) {
83
85
  skip = true;
@@ -90,38 +92,23 @@ function removeTable(input, table) {
90
92
  }
91
93
  return next.join('\n');
92
94
  }
93
- function patchProvider(input, provider, auth, url) {
94
- const table = `model_providers.${provider}`;
95
- const clean = removeTable(input, `${table}.http_headers`);
96
- const lines = clean.split('\n');
97
- const header = new RegExp(`^\\s*\\[\\s*${escape(table)}\\s*\\]\\s*(#.*)?$`);
98
- const index = lines.findIndex((line) => header.test(line));
99
- const keys = providerKeys(auth, url);
100
- if (index === -1)
101
- return `${clean.trimEnd()}\n\n[${table}]\nname = "Sleev"\n${keys.join('\n')}`;
102
- const next = [];
103
- for (let i = 0; i < lines.length; i += 1) {
104
- const line = lines[i] ?? '';
105
- const inside = i > index && !/^\s*\[/.test(line);
106
- if (inside && /^\s*(base_url|wire_api|http_headers)\s*=/.test(line))
107
- continue;
108
- next.push(line);
109
- if (i === index)
110
- next.push(...keys);
111
- }
112
- return next.join('\n');
95
+ function patchProvider(input, auth) {
96
+ return `${input.trimEnd()}\n\n[${table(PROVIDER)}]\nname = "Sleev"\n${providerKeys(auth).join('\n')}`;
113
97
  }
114
- function providerKeys(auth, url) {
98
+ function providerKeys(auth) {
115
99
  return [
116
- `base_url = "${toml(url)}"`,
100
+ `base_url = "${toml(baseUrl(auth.proxyUrl))}"`,
117
101
  'wire_api = "responses"',
118
- `http_headers = { "sleeve-token" = "${toml(auth.token)}", "sleeve-provider" = "openai", "sleeve-harness" = "codex" }`,
102
+ '',
103
+ `[${table(PROVIDER)}.http_headers]`,
104
+ `"sleeve-token" = "${toml(auth.token)}"`,
105
+ '"sleeve-provider" = "openai"',
106
+ `"sleeve-harness" = "${HARNESS}"`,
119
107
  ];
120
108
  }
121
109
  function removeSleevKeys(input, provider) {
122
- const table = `model_providers.${provider}`;
123
110
  const lines = input.split('\n');
124
- const header = new RegExp(`^\\s*\\[\\s*${escape(table)}\\s*\\]\\s*(#.*)?$`);
111
+ const header = new RegExp(`^\\s*\\[\\s*${escape(table(provider))}\\s*\\]\\s*(#.*)?$`);
125
112
  const index = lines.findIndex((line) => header.test(line));
126
113
  if (index === -1)
127
114
  return input;
@@ -132,7 +119,7 @@ function removeSleevKeys(input, provider) {
132
119
  break;
133
120
  current.push(line);
134
121
  }
135
- if (!current.some((line) => line.includes('"sleeve-harness" = "codex"')))
122
+ if (!current.some((line) => owned(line)))
136
123
  return input;
137
124
  return lines
138
125
  .filter((line, i) => {
@@ -143,20 +130,40 @@ function removeSleevKeys(input, provider) {
143
130
  })
144
131
  .join('\n');
145
132
  }
133
+ function previousProvider(input) {
134
+ const current = rootString(input, 'model_provider');
135
+ return backup(input) ?? (current && current !== PROVIDER ? current : null);
136
+ }
137
+ function backup(input) {
138
+ return tableString(input, 'sleev.codex', 'previous_model_provider');
139
+ }
140
+ function tableString(input, name, key) {
141
+ const lines = input.split('\n');
142
+ const header = new RegExp(`^\\s*\\[\\s*${escape(name)}\\s*\\]\\s*(#.*)?$`);
143
+ const index = lines.findIndex((line) => header.test(line));
144
+ if (index === -1)
145
+ return null;
146
+ for (let i = index + 1; i < lines.length; i += 1) {
147
+ const line = lines[i] ?? '';
148
+ if (/^\s*\[/.test(line))
149
+ return null;
150
+ const match = new RegExp(`^\\s*${escape(key)}\\s*=\\s*(["'])([^"']+)\\1`).exec(line);
151
+ if (match?.[2])
152
+ return match[2];
153
+ }
154
+ return null;
155
+ }
146
156
  function rootString(input, key) {
147
157
  const lines = input.split('\n');
148
158
  for (const line of lines) {
149
159
  if (/^\s*\[/.test(line))
150
160
  return null;
151
- const match = new RegExp(`^\\s*${escape(key)}\\s*=\\s*([\"'])([^\"']+)\\1`).exec(line);
161
+ const match = new RegExp(`^\\s*${escape(key)}\\s*=\\s*(["'])([^"']+)\\1`).exec(line);
152
162
  if (match?.[2])
153
163
  return match[2];
154
164
  }
155
165
  return null;
156
166
  }
157
- function reserved(provider) {
158
- return provider === 'openai' || provider === 'ollama' || provider === 'lmstudio';
159
- }
160
167
  async function readText(path) {
161
168
  try {
162
169
  return await readFile(path, 'utf8');
@@ -176,6 +183,23 @@ function escape(value) {
176
183
  function trim(url) {
177
184
  return url.replace(/\/+$/, '');
178
185
  }
186
+ function baseUrl(url) {
187
+ const clean = trim(url);
188
+ if (clean.endsWith('/v1'))
189
+ return clean;
190
+ return `${clean}/v1`;
191
+ }
192
+ function table(provider) {
193
+ return `model_providers.${provider}`;
194
+ }
195
+ function owned(line) {
196
+ return line.includes(`"sleeve-harness" = "${HARNESS}"`) || line.includes(`"sleeve-harness" = "${OLD_HARNESS}"`);
197
+ }
198
+ async function writeText(path, config) {
199
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
200
+ await writeFile(path, config, { mode: 0o600 });
201
+ await chmod(path, 0o600);
202
+ }
179
203
  function code(err) {
180
204
  if (typeof err !== 'object' || err === null || !('code' in err))
181
205
  return undefined;
@@ -48,7 +48,7 @@ export async function loginCommand(options = {}, runtime = commandRuntime()) {
48
48
  success(runtime.output, useLocal ? 'Already logged in' : 'Logged in');
49
49
  field(runtime.output, 'credential', written.path);
50
50
  field(runtime.output, 'proxy', auth.proxyUrl);
51
- note(runtime.output, 'Keep auth.json private. It contains your live Sleeve token');
51
+ note(runtime.output, 'Keep auth.json and patched app configs private. They contain your live Sleeve token');
52
52
  line(runtime.output);
53
53
  const apps = await runtime.selectApps();
54
54
  const harnesses = await runtime.setupHarnesses(auth, apps);
@@ -8,7 +8,7 @@ export async function validateAuth(auth) {
8
8
  'content-type': 'application/json',
9
9
  'sleeve-token': auth.token,
10
10
  'sleeve-provider': 'anthropic',
11
- 'sleeve-harness': 'claude-code',
11
+ 'sleeve-harness': 'claude code',
12
12
  },
13
13
  body: '{}',
14
14
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sleev",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
4
4
  "description": "Sleev command-line tools.",
5
5
  "type": "module",
6
6
  "bin": {