@twigpine/openclaude 0.0.0-stage → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +29 -0
- package/README.md +552 -2
- package/bin/heap-limit.mjs +220 -0
- package/bin/import-specifier.mjs +13 -0
- package/bin/import-specifier.test.mjs +13 -0
- package/bin/node-compile-cache.mjs +17 -0
- package/bin/openclaude +140 -0
- package/dist/cli.mjs +11331 -0
- package/dist/sdk.mjs +286447 -0
- package/docs/windows-aliases-and-launchers.md +176 -0
- package/package.json +219 -4
- package/scripts/windows/openclaude-aliases.ps1 +206 -0
- package/src/entrypoints/sdk/coreTypes.generated.ts +2385 -0
- package/src/entrypoints/sdk.d.ts +621 -0
- package/vendor/node-domexception-shim/index.js +3 -0
- package/vendor/node-domexception-shim/package.json +8 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import os from 'node:os'
|
|
2
|
+
|
|
3
|
+
export const HEAP_SIZE_ENV = 'OPENCLAUDE_NODE_MAX_OLD_SPACE_SIZE_MB'
|
|
4
|
+
export const HEAP_PERCENTAGE_ENV = 'OPENCLAUDE_NODE_MAX_OLD_SPACE_SIZE_PERCENTAGE'
|
|
5
|
+
export const DEFAULT_HEAP_SIZE_MB = 8192
|
|
6
|
+
export const HEAP_SIZE_FLAG = '--max-old-space-size'
|
|
7
|
+
export const HEAP_PERCENTAGE_FLAG = '--max-old-space-size-percentage'
|
|
8
|
+
export const MAX_MEMORY_FLAG = '--max-memory'
|
|
9
|
+
|
|
10
|
+
/** @typedef {{ constrainedMemory?: () => unknown, totalmem?: () => unknown }} MemorySources */
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {string[]} args
|
|
14
|
+
* @param {string} flag
|
|
15
|
+
*/
|
|
16
|
+
export function hasNodeFlag(args, flag) {
|
|
17
|
+
return args.some(arg => arg === flag || arg.startsWith(`${flag}=`))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {string | undefined} nodeOptions
|
|
22
|
+
*/
|
|
23
|
+
export function nodeOptionArgs(nodeOptions) {
|
|
24
|
+
return (nodeOptions || '').split(/\s+/).filter(Boolean)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {string[]} args
|
|
29
|
+
*/
|
|
30
|
+
export function hasHeapLimitFlag(args) {
|
|
31
|
+
return hasNodeFlag(args, HEAP_SIZE_FLAG) || hasNodeFlag(args, HEAP_PERCENTAGE_FLAG)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {unknown} raw
|
|
36
|
+
* @returns {number | null}
|
|
37
|
+
*/
|
|
38
|
+
export function parsePositiveIntegerMb(raw) {
|
|
39
|
+
if (raw == null || raw === '') return null
|
|
40
|
+
const parsed = Number.parseInt(String(raw), 10)
|
|
41
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Node accepts a number greater than 0 and up to 100.
|
|
46
|
+
* @param {unknown} raw
|
|
47
|
+
* @returns {number | null}
|
|
48
|
+
*/
|
|
49
|
+
export function parsePercentage(raw) {
|
|
50
|
+
if (raw == null) return null
|
|
51
|
+
const trimmed = String(raw).trim()
|
|
52
|
+
if (!trimmed) return null
|
|
53
|
+
const withoutPercent = trimmed.endsWith('%') ? trimmed.slice(0, -1) : trimmed
|
|
54
|
+
if (withoutPercent.trim() === '') return null
|
|
55
|
+
const parsed = Number(withoutPercent)
|
|
56
|
+
if (!Number.isFinite(parsed) || parsed <= 0 || parsed > 100) return null
|
|
57
|
+
return parsed
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Prefer cgroup/OS memory constraints so container limits win over host RAM.
|
|
62
|
+
* @param {MemorySources} [sources]
|
|
63
|
+
*/
|
|
64
|
+
export function getAvailableMemoryBytes(sources = {}) {
|
|
65
|
+
const constrainedMemory =
|
|
66
|
+
sources.constrainedMemory ??
|
|
67
|
+
(() => {
|
|
68
|
+
try {
|
|
69
|
+
return typeof process.constrainedMemory === 'function'
|
|
70
|
+
? process.constrainedMemory()
|
|
71
|
+
: undefined
|
|
72
|
+
} catch {
|
|
73
|
+
return undefined
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
const totalmem = sources.totalmem ?? (() => os.totalmem())
|
|
77
|
+
|
|
78
|
+
const constrained = constrainedMemory()
|
|
79
|
+
// Node/libuv reports UINT64_MAX (not a safe integer in JS) when there is no
|
|
80
|
+
// cgroup/OS memory limit. Treat only real byte caps as constraints.
|
|
81
|
+
if (typeof constrained === 'number' && Number.isSafeInteger(constrained) && constrained > 0) {
|
|
82
|
+
return constrained
|
|
83
|
+
}
|
|
84
|
+
const total = totalmem()
|
|
85
|
+
if (typeof total === 'number' && Number.isFinite(total) && total > 0) {
|
|
86
|
+
return total
|
|
87
|
+
}
|
|
88
|
+
return 0
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @param {number} percentage
|
|
93
|
+
* @param {number} availableBytes
|
|
94
|
+
* @returns {number | null}
|
|
95
|
+
*/
|
|
96
|
+
export function heapSizeMbFromPercentage(percentage, availableBytes) {
|
|
97
|
+
if (!(percentage > 0 && percentage <= 100) || !(availableBytes > 0)) return null
|
|
98
|
+
const mb = Math.floor((availableBytes * (percentage / 100)) / (1024 * 1024))
|
|
99
|
+
return Math.max(1, mb)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @param {string[]} args
|
|
104
|
+
* @param {string} flag
|
|
105
|
+
* @returns {string | null}
|
|
106
|
+
*/
|
|
107
|
+
export function findEqualsFlagValue(args, flag) {
|
|
108
|
+
const prefix = `${flag}=`
|
|
109
|
+
for (const arg of args) {
|
|
110
|
+
if (arg.startsWith(prefix)) return arg.slice(prefix.length)
|
|
111
|
+
}
|
|
112
|
+
return null
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* @param {string[]} args
|
|
117
|
+
* @param {string} flag
|
|
118
|
+
* @returns {string | null}
|
|
119
|
+
*/
|
|
120
|
+
export function findEqualsOrNextFlagValue(args, flag) {
|
|
121
|
+
const prefix = `${flag}=`
|
|
122
|
+
for (let i = 0; i < args.length; i++) {
|
|
123
|
+
const arg = args[i]
|
|
124
|
+
if (arg.startsWith(prefix)) return arg.slice(prefix.length)
|
|
125
|
+
if (arg === flag) {
|
|
126
|
+
const next = args[i + 1]
|
|
127
|
+
if (next && !next.startsWith('-')) return next
|
|
128
|
+
return ''
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return null
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Strip launcher-only heap args so Commander does not reject them.
|
|
136
|
+
* `--max-memory` stays equals-only to match the previous launcher contract.
|
|
137
|
+
* @param {string[]} args
|
|
138
|
+
*/
|
|
139
|
+
export function stripLauncherHeapArgs(args) {
|
|
140
|
+
const stripped = []
|
|
141
|
+
for (let i = 0; i < args.length; i++) {
|
|
142
|
+
const arg = args[i]
|
|
143
|
+
if (arg === MAX_MEMORY_FLAG || arg.startsWith(`${MAX_MEMORY_FLAG}=`)) continue
|
|
144
|
+
if (arg.startsWith(`${HEAP_PERCENTAGE_FLAG}=`)) continue
|
|
145
|
+
if (arg === HEAP_PERCENTAGE_FLAG) {
|
|
146
|
+
const next = args[i + 1]
|
|
147
|
+
if (next && !next.startsWith('-') && parsePercentage(next) != null) i += 1
|
|
148
|
+
continue
|
|
149
|
+
}
|
|
150
|
+
stripped.push(arg)
|
|
151
|
+
}
|
|
152
|
+
return stripped
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* @param {{
|
|
157
|
+
* argv?: string[],
|
|
158
|
+
* env?: NodeJS.ProcessEnv,
|
|
159
|
+
* availableBytes?: number,
|
|
160
|
+
* memorySources?: MemorySources,
|
|
161
|
+
* }} [input]
|
|
162
|
+
*/
|
|
163
|
+
export function resolveHeapSizeMb(input = {}) {
|
|
164
|
+
const argv = input.argv ?? []
|
|
165
|
+
const env = input.env ?? {}
|
|
166
|
+
const availableBytes =
|
|
167
|
+
input.availableBytes ?? getAvailableMemoryBytes(input.memorySources)
|
|
168
|
+
|
|
169
|
+
const maxMem = parsePositiveIntegerMb(findEqualsFlagValue(argv, MAX_MEMORY_FLAG))
|
|
170
|
+
if (maxMem != null) {
|
|
171
|
+
return { mb: maxMem, source: 'max-memory', setMaxMemoryEnv: true }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const argvPercentage = parsePercentage(
|
|
175
|
+
findEqualsOrNextFlagValue(argv, HEAP_PERCENTAGE_FLAG),
|
|
176
|
+
)
|
|
177
|
+
if (argvPercentage != null) {
|
|
178
|
+
const mb = heapSizeMbFromPercentage(argvPercentage, availableBytes)
|
|
179
|
+
if (mb != null) {
|
|
180
|
+
return { mb, source: 'argv-percentage', percentage: argvPercentage }
|
|
181
|
+
}
|
|
182
|
+
// Valid percentage requested; memory size unknown. Do not treat this as
|
|
183
|
+
// "no percentage" and fall through to an unrelated MB env value.
|
|
184
|
+
return {
|
|
185
|
+
mb: DEFAULT_HEAP_SIZE_MB,
|
|
186
|
+
source: 'percentage-unavailable',
|
|
187
|
+
percentage: argvPercentage,
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const envPercentage = parsePercentage(env[HEAP_PERCENTAGE_ENV])
|
|
192
|
+
if (envPercentage != null) {
|
|
193
|
+
const mb = heapSizeMbFromPercentage(envPercentage, availableBytes)
|
|
194
|
+
if (mb != null) {
|
|
195
|
+
return { mb, source: 'env-percentage', percentage: envPercentage }
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
mb: DEFAULT_HEAP_SIZE_MB,
|
|
199
|
+
source: 'percentage-unavailable',
|
|
200
|
+
percentage: envPercentage,
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const envMb = parsePositiveIntegerMb(env[HEAP_SIZE_ENV])
|
|
205
|
+
if (envMb != null) return { mb: envMb, source: 'env-mb' }
|
|
206
|
+
|
|
207
|
+
return { mb: DEFAULT_HEAP_SIZE_MB, source: 'default' }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* One-line operator warning when a valid percentage cannot be converted.
|
|
212
|
+
* @param {number} percentage
|
|
213
|
+
* @param {number} [fallbackMb]
|
|
214
|
+
*/
|
|
215
|
+
export function formatPercentageUnavailableStderr(
|
|
216
|
+
percentage,
|
|
217
|
+
fallbackMb = DEFAULT_HEAP_SIZE_MB,
|
|
218
|
+
) {
|
|
219
|
+
return `openclaude: could not convert heap percentage ${percentage} to megabytes because available memory is unknown; using ${fallbackMb}`
|
|
220
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { join, win32 } from 'path'
|
|
2
|
+
import { pathToFileURL } from 'url'
|
|
3
|
+
|
|
4
|
+
export function getDistImportSpecifier(baseDir) {
|
|
5
|
+
if (/^[A-Za-z]:\\/.test(baseDir)) {
|
|
6
|
+
const distPath = win32.join(baseDir, '..', 'dist', 'cli.mjs')
|
|
7
|
+
return `file:///${distPath.replace(/\\/g, '/')}`
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const joinImpl = join
|
|
11
|
+
const distPath = joinImpl(baseDir, '..', 'dist', 'cli.mjs')
|
|
12
|
+
return pathToFileURL(distPath).href
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import test from 'node:test'
|
|
3
|
+
|
|
4
|
+
import { getDistImportSpecifier } from './import-specifier.mjs'
|
|
5
|
+
|
|
6
|
+
test('builds a file URL import specifier for dist/cli.mjs', () => {
|
|
7
|
+
const specifier = getDistImportSpecifier('C:\\repo\\bin')
|
|
8
|
+
|
|
9
|
+
assert.equal(
|
|
10
|
+
specifier,
|
|
11
|
+
'file:///C:/repo/dist/cli.mjs',
|
|
12
|
+
)
|
|
13
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as nodeModule from 'node:module'
|
|
2
|
+
|
|
3
|
+
/** @typedef {{ enableCompileCache?: () => unknown }} CompileCacheModule */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {CompileCacheModule} [module=nodeModule]
|
|
7
|
+
*/
|
|
8
|
+
export function enableNodeCompileCacheIfAvailable(module = nodeModule) {
|
|
9
|
+
const enable = module.enableCompileCache
|
|
10
|
+
if (typeof enable !== 'function') return
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
enable()
|
|
14
|
+
} catch {
|
|
15
|
+
// Compile caching is optional and must never block startup.
|
|
16
|
+
}
|
|
17
|
+
}
|
package/bin/openclaude
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* OpenClaude — Claude Code with any LLM
|
|
5
|
+
*
|
|
6
|
+
* If dist/cli.mjs exists (built), run that.
|
|
7
|
+
* Otherwise, tell the user to build first or use `bun run dev`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync } from 'fs'
|
|
11
|
+
import { join, dirname } from 'path'
|
|
12
|
+
import { fileURLToPath, pathToFileURL } from 'url'
|
|
13
|
+
import { spawnSync } from 'child_process'
|
|
14
|
+
import { enableNodeCompileCacheIfAvailable } from './node-compile-cache.mjs'
|
|
15
|
+
import {
|
|
16
|
+
HEAP_SIZE_ENV,
|
|
17
|
+
HEAP_SIZE_FLAG,
|
|
18
|
+
formatPercentageUnavailableStderr,
|
|
19
|
+
hasHeapLimitFlag,
|
|
20
|
+
hasNodeFlag,
|
|
21
|
+
nodeOptionArgs,
|
|
22
|
+
resolveHeapSizeMb,
|
|
23
|
+
stripLauncherHeapArgs,
|
|
24
|
+
} from './heap-limit.mjs'
|
|
25
|
+
|
|
26
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
27
|
+
const distPath = join(__dirname, '..', 'dist', 'cli.mjs')
|
|
28
|
+
|
|
29
|
+
const HEAP_RELAUNCHED_ENV = 'OPENCLAUDE_HEAP_RELAUNCHED'
|
|
30
|
+
const DISABLE_HEAP_RELAUNCH_ENV = 'OPENCLAUDE_DISABLE_HEAP_RELAUNCH'
|
|
31
|
+
|
|
32
|
+
function currentNodeOptionArgs() {
|
|
33
|
+
return [...process.execArgv, ...nodeOptionArgs(process.env.NODE_OPTIONS)]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function applyResolvedHeapEnv(resolved) {
|
|
37
|
+
process.env[HEAP_SIZE_ENV] = String(resolved.mb)
|
|
38
|
+
if (resolved.setMaxMemoryEnv) {
|
|
39
|
+
process.env.OPENCLAUDE_MAX_MEMORY_MB = String(resolved.mb)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function replaceProcessArgvWithStrippedLauncherArgs() {
|
|
44
|
+
process.argv = [
|
|
45
|
+
process.argv[0],
|
|
46
|
+
process.argv[1],
|
|
47
|
+
...stripLauncherHeapArgs(process.argv.slice(2)),
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function relaunchWithLongSessionHeapIfNeeded() {
|
|
52
|
+
if (process.env[DISABLE_HEAP_RELAUNCH_ENV] === '1') {
|
|
53
|
+
replaceProcessArgvWithStrippedLauncherArgs()
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
if (process.env[HEAP_RELAUNCHED_ENV] === '1') {
|
|
57
|
+
replaceProcessArgvWithStrippedLauncherArgs()
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
const nodeArgs = currentNodeOptionArgs()
|
|
61
|
+
const hasHeapLimit = hasHeapLimitFlag(nodeArgs)
|
|
62
|
+
const hasExplicitGc = hasNodeFlag(nodeArgs, '--expose-gc')
|
|
63
|
+
if (hasHeapLimit && hasExplicitGc) {
|
|
64
|
+
replaceProcessArgvWithStrippedLauncherArgs()
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const execArgv = [...process.execArgv]
|
|
69
|
+
if (!hasHeapLimit) {
|
|
70
|
+
const resolved = resolveHeapSizeMb({
|
|
71
|
+
argv: process.argv.slice(2),
|
|
72
|
+
env: process.env,
|
|
73
|
+
})
|
|
74
|
+
if (resolved.source === 'percentage-unavailable') {
|
|
75
|
+
console.error(formatPercentageUnavailableStderr(resolved.percentage))
|
|
76
|
+
}
|
|
77
|
+
applyResolvedHeapEnv(resolved)
|
|
78
|
+
execArgv.push(`${HEAP_SIZE_FLAG}=${resolved.mb}`)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Expose explicit GC for long interactive sessions. NODE_OPTIONS cannot
|
|
82
|
+
// carry --expose-gc, so the executable wrapper must add it before startup.
|
|
83
|
+
if (!hasExplicitGc) {
|
|
84
|
+
execArgv.push('--expose-gc')
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Strip launcher-only heap args before relaunching — Commander in the built
|
|
88
|
+
// CLI would reject them as unknown. The native Node percentage flag in
|
|
89
|
+
// execArgv/NODE_OPTIONS is left untouched because this process already
|
|
90
|
+
// started with a Node that accepted it.
|
|
91
|
+
const childArgs = stripLauncherHeapArgs(process.argv.slice(2))
|
|
92
|
+
|
|
93
|
+
// Preserve the original argv[1] (which may be a symlink like
|
|
94
|
+
// /usr/local/bin/openclaude) instead of resolving it via import.meta.url.
|
|
95
|
+
// Resolving symlinks here defeats install-type detection downstream: a real
|
|
96
|
+
// npm global install (symlink → node_modules/@twigpine/openclaude/bin) would
|
|
97
|
+
// resolve to the package's real path inside node_modules, which is fine, but
|
|
98
|
+
// a `npm install -g .` dev symlink resolves back to the repo and looks like
|
|
99
|
+
// a source-tree dev run. Using argv[1] keeps the invocation path stable so
|
|
100
|
+
// doctorDiagnostic's npm-global path markers can match correctly.
|
|
101
|
+
const launcherPath = process.argv[1] || fileURLToPath(import.meta.url)
|
|
102
|
+
|
|
103
|
+
const result = spawnSync(process.execPath, [
|
|
104
|
+
...execArgv,
|
|
105
|
+
launcherPath,
|
|
106
|
+
...childArgs,
|
|
107
|
+
], {
|
|
108
|
+
stdio: 'inherit',
|
|
109
|
+
env: {
|
|
110
|
+
...process.env,
|
|
111
|
+
[HEAP_RELAUNCHED_ENV]: '1',
|
|
112
|
+
},
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
if (result.error) {
|
|
116
|
+
console.error(`openclaude: failed to restart with long-session heap: ${result.error.message}`)
|
|
117
|
+
process.exit(1)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
process.exit(result.status ?? 1)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (existsSync(distPath)) {
|
|
124
|
+
relaunchWithLongSessionHeapIfNeeded()
|
|
125
|
+
enableNodeCompileCacheIfAvailable()
|
|
126
|
+
await import(pathToFileURL(distPath).href)
|
|
127
|
+
} else {
|
|
128
|
+
console.error(`
|
|
129
|
+
openclaude: dist/cli.mjs not found.
|
|
130
|
+
|
|
131
|
+
Build first:
|
|
132
|
+
bun run build
|
|
133
|
+
|
|
134
|
+
Or run directly with Bun:
|
|
135
|
+
bun run dev
|
|
136
|
+
|
|
137
|
+
See README.md for setup instructions.
|
|
138
|
+
`)
|
|
139
|
+
process.exit(1)
|
|
140
|
+
}
|