@shareai-lab/kode 1.0.83 → 1.0.91
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/cli.js +43 -56
- package/package.json +1 -1
- package/scripts/postinstall.js +11 -49
- package/src/commands/agents.tsx +10 -1
- package/src/components/AsciiLogo.tsx +1 -1
- package/src/components/CustomSelect/select-option.tsx +2 -2
- package/src/components/Help.tsx +2 -2
- package/src/components/Logo.tsx +27 -2
- package/src/components/ModelStatusDisplay.tsx +3 -3
- package/src/components/Onboarding.tsx +2 -2
- package/src/components/PromptInput.tsx +3 -3
- package/src/components/Spinner.tsx +3 -3
- package/src/components/messages/TaskProgressMessage.tsx +2 -2
- package/src/components/messages/UserKodingInputMessage.tsx +1 -1
- package/src/entrypoints/cli.tsx +62 -38
- package/src/screens/REPL.tsx +19 -1
- package/src/services/openai.ts +1 -1
- package/src/services/statsig.ts +3 -2
- package/src/utils/agentLoader.ts +11 -17
- package/src/utils/autoUpdater.ts +171 -31
- package/src/utils/config.ts +3 -0
- package/src/utils/file.ts +6 -3
- package/src/utils/permissions/filesystem.ts +30 -21
- package/src/utils/secureFile.ts +8 -3
- package/src/utils/theme.ts +27 -33
package/cli.js
CHANGED
|
@@ -3,84 +3,71 @@
|
|
|
3
3
|
const { spawn } = require('child_process');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
|
|
6
|
-
// Prefer
|
|
6
|
+
// Prefer Bun for dev; otherwise use Node + tsx (ESM --import)
|
|
7
7
|
const args = process.argv.slice(2);
|
|
8
8
|
const cliPath = path.join(__dirname, 'src', 'entrypoints', 'cli.tsx');
|
|
9
9
|
|
|
10
|
-
//
|
|
10
|
+
// Resolve tsx from this package directory (avoid CWD-based resolution on Windows/global installs)
|
|
11
|
+
let tsxImportPath = null;
|
|
12
|
+
try { tsxImportPath = require.resolve('tsx', { paths: [__dirname] }); } catch {}
|
|
13
|
+
|
|
14
|
+
// Enforce minimum Node version based on package.json engines.node
|
|
15
|
+
try {
|
|
16
|
+
const req = "${enginesNode}"
|
|
17
|
+
const m = req.match(/(\d+)\.(\d+)\.(\d+)/)
|
|
18
|
+
const [maj, min, pat] = process.versions.node.split('.').map(Number)
|
|
19
|
+
const [rMaj, rMin, rPat] = m ? m.slice(1).map(Number) : [20, 18, 1]
|
|
20
|
+
const tooOld = (maj < rMaj) || (maj === rMaj && (min < rMin || (min === rMin && pat < rPat)))
|
|
21
|
+
if (tooOld) {
|
|
22
|
+
console.error(
|
|
23
|
+
'Error: Node.js ' + req + ' is required. Please upgrade Node.'
|
|
24
|
+
);
|
|
25
|
+
process.exit(1)
|
|
26
|
+
}
|
|
27
|
+
} catch {}
|
|
28
|
+
|
|
29
|
+
// Try Bun first
|
|
11
30
|
try {
|
|
12
31
|
const { execSync } = require('child_process');
|
|
13
32
|
execSync('bun --version', { stdio: 'ignore' });
|
|
14
|
-
|
|
15
|
-
// Bun is available
|
|
16
33
|
const child = spawn('bun', ['run', cliPath, ...args], {
|
|
17
34
|
stdio: 'inherit',
|
|
18
|
-
env: {
|
|
19
|
-
...process.env,
|
|
20
|
-
YOGA_WASM_PATH: path.join(__dirname, 'yoga.wasm')
|
|
21
|
-
}
|
|
35
|
+
env: { ...process.env, YOGA_WASM_PATH: path.join(__dirname, 'yoga.wasm') },
|
|
22
36
|
});
|
|
23
|
-
|
|
24
37
|
child.on('exit', (code) => process.exit(code || 0));
|
|
25
|
-
child.on('error', () =>
|
|
26
|
-
|
|
27
|
-
runWithNode();
|
|
28
|
-
});
|
|
29
|
-
} catch {
|
|
30
|
-
// Bun not available, use node
|
|
31
|
-
runWithNode();
|
|
32
|
-
}
|
|
38
|
+
child.on('error', () => runWithNode());
|
|
39
|
+
} catch { runWithNode(); }
|
|
33
40
|
|
|
34
41
|
function runWithNode() {
|
|
35
|
-
const nodeVersion = process.versions.node.split('.').map(Number)
|
|
36
|
-
const [major, minor] = nodeVersion
|
|
37
|
-
const supportsImport = major > 20 || (major === 20 && minor >= 6) || major >= 21
|
|
38
|
-
|
|
39
42
|
const baseArgs = ['--no-warnings=ExperimentalWarning', '--enable-source-maps']
|
|
40
43
|
|
|
41
|
-
const
|
|
42
|
-
|
|
44
|
+
const tryWithImport = () => {
|
|
45
|
+
let importArg = 'tsx'
|
|
46
|
+
try {
|
|
47
|
+
if (tsxImportPath) {
|
|
48
|
+
const { pathToFileURL } = require('node:url')
|
|
49
|
+
importArg = pathToFileURL(tsxImportPath).href
|
|
50
|
+
}
|
|
51
|
+
} catch {}
|
|
52
|
+
const child = spawn(process.execPath, [...baseArgs, '--import', importArg, cliPath, ...args], {
|
|
43
53
|
stdio: 'inherit',
|
|
44
54
|
env: { ...process.env, YOGA_WASM_PATH: path.join(__dirname, 'yoga.wasm') },
|
|
45
55
|
})
|
|
46
|
-
child.on('error', () =>
|
|
56
|
+
child.on('error', () => failWithTsxHint())
|
|
47
57
|
child.on('exit', code => {
|
|
48
|
-
if (code && code !== 0) return
|
|
58
|
+
if (code && code !== 0) return failWithTsxHint()
|
|
49
59
|
process.exit(code || 0)
|
|
50
60
|
})
|
|
51
61
|
}
|
|
52
62
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
child.on('error', err => {
|
|
60
|
-
if (err.code === 'ENOENT') {
|
|
61
|
-
console.error('
|
|
62
|
-
Error: tsx is required but not found.')
|
|
63
|
-
console.error('Please run: npm install')
|
|
64
|
-
process.exit(1)
|
|
65
|
-
} else {
|
|
66
|
-
console.error('Failed to start Kode:', err.message)
|
|
67
|
-
process.exit(1)
|
|
68
|
-
}
|
|
69
|
-
})
|
|
70
|
-
child.on('exit', code => process.exit(code || 0))
|
|
63
|
+
function failWithTsxHint() {
|
|
64
|
+
console.error('')
|
|
65
|
+
console.error('Failed to launch with Node + tsx.')
|
|
66
|
+
console.error('Try reinstalling: npm i -g @shareai-lab/kode (ensures bundled tsx).')
|
|
67
|
+
console.error('Or install Bun and run: bun run kode')
|
|
68
|
+
process.exit(1)
|
|
71
69
|
}
|
|
72
70
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
stdio: 'inherit',
|
|
76
|
-
env: { ...process.env, YOGA_WASM_PATH: path.join(__dirname, 'yoga.wasm') },
|
|
77
|
-
})
|
|
78
|
-
child.on('error', () => tryWithLoader())
|
|
79
|
-
child.on('exit', code => {
|
|
80
|
-
if (code && code !== 0) return tryWithLoader()
|
|
81
|
-
process.exit(code || 0)
|
|
82
|
-
})
|
|
83
|
-
} else {
|
|
84
|
-
tryWithLoader()
|
|
85
|
-
}
|
|
71
|
+
// Single modern path: Node >= 20 + tsx via --import; fallback to tsx CLI
|
|
72
|
+
tryWithImport()
|
|
86
73
|
}
|
package/package.json
CHANGED
package/scripts/postinstall.js
CHANGED
|
@@ -1,56 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
// This postinstall is intentionally minimal and cross-platform safe.
|
|
4
|
+
// npm/pnpm/yarn already create shims from package.json "bin" fields.
|
|
5
|
+
// We avoid attempting to create symlinks or relying on platform-specific tools like `which`/`where`.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
function commandExists(cmd) {
|
|
7
|
+
function postinstallNotice() {
|
|
8
|
+
// Only print informational hints; never fail install.
|
|
11
9
|
try {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function setupCommand() {
|
|
20
|
-
// Check if primary command exists
|
|
21
|
-
if (!commandExists(primaryCommand)) {
|
|
22
|
-
console.log(`✅ '${primaryCommand}' command is available and has been set up.`);
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
console.log(`⚠️ '${primaryCommand}' command already exists on your system.`);
|
|
27
|
-
|
|
28
|
-
// Find an available alternative
|
|
29
|
-
for (const alt of alternativeCommands) {
|
|
30
|
-
if (!commandExists(alt)) {
|
|
31
|
-
// Create alternative command
|
|
32
|
-
const binPath = path.join(__dirname, '..', 'cli.js');
|
|
33
|
-
const altBinPath = path.join(__dirname, '..', '..', '..', '.bin', alt);
|
|
34
|
-
|
|
35
|
-
try {
|
|
36
|
-
fs.symlinkSync(binPath, altBinPath);
|
|
37
|
-
console.log(`✅ Created alternative command '${alt}' instead.`);
|
|
38
|
-
console.log(` You can run the tool using: ${alt}`);
|
|
39
|
-
return;
|
|
40
|
-
} catch (err) {
|
|
41
|
-
// Continue to next alternative
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
console.log(`
|
|
47
|
-
⚠️ All common command names are taken. You can still run the tool using:
|
|
48
|
-
- npx @shareai-lab/kode
|
|
49
|
-
- Or create your own alias: alias myai='npx @shareai-lab/kode'
|
|
50
|
-
`);
|
|
10
|
+
console.log('✅ @shareai-lab/kode installed. Commands available: kode, kwa, kd');
|
|
11
|
+
console.log(' If shell cannot find them, try reloading your terminal or reinstall globally:');
|
|
12
|
+
console.log(' npm i -g @shareai-lab/kode (or use: npx @shareai-lab/kode)');
|
|
13
|
+
} catch {}
|
|
51
14
|
}
|
|
52
15
|
|
|
53
|
-
// Only run in postinstall, not in development
|
|
54
16
|
if (process.env.npm_lifecycle_event === 'postinstall') {
|
|
55
|
-
|
|
56
|
-
}
|
|
17
|
+
postinstallNotice();
|
|
18
|
+
}
|
package/src/commands/agents.tsx
CHANGED
|
@@ -462,7 +462,16 @@ async function openInEditor(filePath: string): Promise<void> {
|
|
|
462
462
|
const projectDir = process.cwd()
|
|
463
463
|
const homeDir = os.homedir()
|
|
464
464
|
|
|
465
|
-
|
|
465
|
+
const isSub = (base: string, target: string) => {
|
|
466
|
+
const path = require('path')
|
|
467
|
+
const rel = path.relative(path.resolve(base), path.resolve(target))
|
|
468
|
+
if (!rel || rel === '') return true
|
|
469
|
+
if (rel.startsWith('..')) return false
|
|
470
|
+
if (path.isAbsolute(rel)) return false
|
|
471
|
+
return true
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (!isSub(projectDir, resolvedPath) && !isSub(homeDir, resolvedPath)) {
|
|
466
475
|
throw new Error('Access denied: File path outside allowed directories')
|
|
467
476
|
}
|
|
468
477
|
|
|
@@ -45,13 +45,13 @@ export function SelectOption({
|
|
|
45
45
|
paddingRight: 1,
|
|
46
46
|
}),
|
|
47
47
|
focusIndicator: () => ({
|
|
48
|
-
color: appTheme.
|
|
48
|
+
color: appTheme.kode,
|
|
49
49
|
}),
|
|
50
50
|
label: ({ isFocused, isSelected }: { isFocused: boolean; isSelected: boolean }) => ({
|
|
51
51
|
color: isSelected
|
|
52
52
|
? appTheme.success
|
|
53
53
|
: isFocused
|
|
54
|
-
? appTheme.
|
|
54
|
+
? appTheme.kode
|
|
55
55
|
: appTheme.text,
|
|
56
56
|
bold: isSelected,
|
|
57
57
|
}),
|
package/src/components/Help.tsx
CHANGED
|
@@ -66,7 +66,7 @@ export function Help({
|
|
|
66
66
|
|
|
67
67
|
return (
|
|
68
68
|
<Box flexDirection="column" padding={1}>
|
|
69
|
-
<Text bold color={theme.
|
|
69
|
+
<Text bold color={theme.kode}>
|
|
70
70
|
{`${PRODUCT_NAME} v${MACRO.VERSION}`}
|
|
71
71
|
</Text>
|
|
72
72
|
|
|
@@ -150,7 +150,7 @@ export function Help({
|
|
|
150
150
|
<Box flexDirection="column">
|
|
151
151
|
{customCommands.map((cmd, i) => (
|
|
152
152
|
<Box key={i} marginLeft={1}>
|
|
153
|
-
<Text bold color={theme.
|
|
153
|
+
<Text bold color={theme.kode}>{`/${cmd.name}`}</Text>
|
|
154
154
|
<Text> - {cmd.description}</Text>
|
|
155
155
|
{cmd.aliases && cmd.aliases.length > 0 && (
|
|
156
156
|
<Text color={theme.secondaryText}>
|
package/src/components/Logo.tsx
CHANGED
|
@@ -13,9 +13,13 @@ export const MIN_LOGO_WIDTH = 50
|
|
|
13
13
|
export function Logo({
|
|
14
14
|
mcpClients,
|
|
15
15
|
isDefaultModel = false,
|
|
16
|
+
updateBannerVersion,
|
|
17
|
+
updateBannerCommands,
|
|
16
18
|
}: {
|
|
17
19
|
mcpClients: WrappedClient[]
|
|
18
20
|
isDefaultModel?: boolean
|
|
21
|
+
updateBannerVersion?: string | null
|
|
22
|
+
updateBannerCommands?: string[] | null
|
|
19
23
|
}): React.ReactNode {
|
|
20
24
|
const width = Math.max(MIN_LOGO_WIDTH, getCwd().length + 12)
|
|
21
25
|
const theme = getTheme()
|
|
@@ -35,15 +39,36 @@ export function Logo({
|
|
|
35
39
|
return (
|
|
36
40
|
<Box flexDirection="column">
|
|
37
41
|
<Box
|
|
38
|
-
borderColor={theme.
|
|
42
|
+
borderColor={theme.kode}
|
|
39
43
|
borderStyle="round"
|
|
40
44
|
flexDirection="column"
|
|
41
45
|
gap={1}
|
|
42
46
|
paddingLeft={1}
|
|
47
|
+
marginRight={2}
|
|
43
48
|
width={width}
|
|
44
49
|
>
|
|
50
|
+
{updateBannerVersion ? (
|
|
51
|
+
<Box flexDirection="column">
|
|
52
|
+
<Text color="yellow">New version available: {updateBannerVersion}</Text>
|
|
53
|
+
<Text>Run the following command to update:</Text>
|
|
54
|
+
<Text>
|
|
55
|
+
{' '}
|
|
56
|
+
{updateBannerCommands?.[0] ?? 'bun add -g @shareai-lab/kode@latest'}
|
|
57
|
+
</Text>
|
|
58
|
+
<Text>Or:</Text>
|
|
59
|
+
<Text>
|
|
60
|
+
{' '}
|
|
61
|
+
{updateBannerCommands?.[1] ?? 'npm install -g @shareai-lab/kode@latest'}
|
|
62
|
+
</Text>
|
|
63
|
+
{process.platform !== 'win32' && (
|
|
64
|
+
<Text dimColor>
|
|
65
|
+
Note: you may need to prefix with "sudo" on macOS/Linux.
|
|
66
|
+
</Text>
|
|
67
|
+
)}
|
|
68
|
+
</Box>
|
|
69
|
+
) : null}
|
|
45
70
|
<Text>
|
|
46
|
-
<Text color={theme.
|
|
71
|
+
<Text color={theme.kode}>✻</Text> Welcome to{' '}
|
|
47
72
|
<Text bold>{PRODUCT_NAME}</Text> <Text>research preview!</Text>
|
|
48
73
|
</Text>
|
|
49
74
|
{/* <AsciiLogo /> */}
|
|
@@ -44,7 +44,7 @@ export function ModelStatusDisplay({ onClose }: Props): React.ReactNode {
|
|
|
44
44
|
<Box key={pointer} flexDirection="column" marginBottom={1}>
|
|
45
45
|
<Text>
|
|
46
46
|
🎯{' '}
|
|
47
|
-
<Text bold color={theme.
|
|
47
|
+
<Text bold color={theme.kode}>
|
|
48
48
|
{pointer.toUpperCase()}
|
|
49
49
|
</Text>{' '}
|
|
50
50
|
→ {model.name}
|
|
@@ -76,7 +76,7 @@ export function ModelStatusDisplay({ onClose }: Props): React.ReactNode {
|
|
|
76
76
|
<Box key={pointer} flexDirection="column" marginBottom={1}>
|
|
77
77
|
<Text>
|
|
78
78
|
🎯{' '}
|
|
79
|
-
<Text bold color={theme.
|
|
79
|
+
<Text bold color={theme.kode}>
|
|
80
80
|
{pointer.toUpperCase()}
|
|
81
81
|
</Text>{' '}
|
|
82
82
|
→ <Text color={theme.error}>❌ Not configured</Text>
|
|
@@ -89,7 +89,7 @@ export function ModelStatusDisplay({ onClose }: Props): React.ReactNode {
|
|
|
89
89
|
<Box key={pointer} flexDirection="column" marginBottom={1}>
|
|
90
90
|
<Text>
|
|
91
91
|
🎯{' '}
|
|
92
|
-
<Text bold color={theme.
|
|
92
|
+
<Text bold color={theme.kode}>
|
|
93
93
|
{pointer.toUpperCase()}
|
|
94
94
|
</Text>{' '}
|
|
95
95
|
→{' '}
|
|
@@ -260,13 +260,13 @@ export function WelcomeBox(): React.ReactNode {
|
|
|
260
260
|
const theme = getTheme()
|
|
261
261
|
return (
|
|
262
262
|
<Box
|
|
263
|
-
borderColor={theme.
|
|
263
|
+
borderColor={theme.kode}
|
|
264
264
|
borderStyle="round"
|
|
265
265
|
paddingX={1}
|
|
266
266
|
width={MIN_LOGO_WIDTH}
|
|
267
267
|
>
|
|
268
268
|
<Text>
|
|
269
|
-
<Text color={theme.
|
|
269
|
+
<Text color={theme.kode}>✻</Text> Welcome to{' '}
|
|
270
270
|
<Text bold>{PRODUCT_NAME}</Text> research preview!
|
|
271
271
|
</Text>
|
|
272
272
|
</Box>
|
|
@@ -596,7 +596,7 @@ function PromptInput({
|
|
|
596
596
|
mode === 'bash'
|
|
597
597
|
? theme.bashBorder
|
|
598
598
|
: mode === 'koding'
|
|
599
|
-
? theme.
|
|
599
|
+
? theme.noting
|
|
600
600
|
: theme.secondaryBorder
|
|
601
601
|
}
|
|
602
602
|
borderDimColor
|
|
@@ -614,7 +614,7 @@ function PromptInput({
|
|
|
614
614
|
{mode === 'bash' ? (
|
|
615
615
|
<Text color={theme.bashBorder}> ! </Text>
|
|
616
616
|
) : mode === 'koding' ? (
|
|
617
|
-
<Text color={theme.
|
|
617
|
+
<Text color={theme.noting}> # </Text>
|
|
618
618
|
) : (
|
|
619
619
|
<Text color={isLoading ? theme.secondaryText : undefined}>
|
|
620
620
|
>
|
|
@@ -668,7 +668,7 @@ function PromptInput({
|
|
|
668
668
|
! for bash mode
|
|
669
669
|
</Text>
|
|
670
670
|
<Text
|
|
671
|
-
color={mode === 'koding' ? theme.
|
|
671
|
+
color={mode === 'koding' ? theme.noting : undefined}
|
|
672
672
|
dimColor={mode !== 'koding'}
|
|
673
673
|
>
|
|
674
674
|
· # for AGENTS.md
|
|
@@ -96,9 +96,9 @@ export function Spinner(): React.ReactNode {
|
|
|
96
96
|
return (
|
|
97
97
|
<Box flexDirection="row" marginTop={1}>
|
|
98
98
|
<Box flexWrap="nowrap" height={1} width={2}>
|
|
99
|
-
<Text color={getTheme().
|
|
99
|
+
<Text color={getTheme().kode}>{frames[frame]}</Text>
|
|
100
100
|
</Box>
|
|
101
|
-
<Text color={getTheme().
|
|
101
|
+
<Text color={getTheme().kode}>{message.current}… </Text>
|
|
102
102
|
<Text color={getTheme().secondaryText}>
|
|
103
103
|
({elapsedTime}s · <Text bold>esc</Text> to interrupt)
|
|
104
104
|
</Text>
|
|
@@ -123,7 +123,7 @@ export function SimpleSpinner(): React.ReactNode {
|
|
|
123
123
|
|
|
124
124
|
return (
|
|
125
125
|
<Box flexWrap="nowrap" height={1} width={2}>
|
|
126
|
-
<Text color={getTheme().
|
|
126
|
+
<Text color={getTheme().kode}>{frames[frame]}</Text>
|
|
127
127
|
</Box>
|
|
128
128
|
)
|
|
129
129
|
}
|
|
@@ -14,7 +14,7 @@ export function TaskProgressMessage({ agentType, status, toolCount }: Props) {
|
|
|
14
14
|
return (
|
|
15
15
|
<Box flexDirection="column" marginTop={1}>
|
|
16
16
|
<Box flexDirection="row">
|
|
17
|
-
<Text color={theme.
|
|
17
|
+
<Text color={theme.kode}>⎯ </Text>
|
|
18
18
|
<Text color={theme.text} bold>
|
|
19
19
|
[{agentType}]
|
|
20
20
|
</Text>
|
|
@@ -29,4 +29,4 @@ export function TaskProgressMessage({ agentType, status, toolCount }: Props) {
|
|
|
29
29
|
)}
|
|
30
30
|
</Box>
|
|
31
31
|
)
|
|
32
|
-
}
|
|
32
|
+
}
|
|
@@ -20,7 +20,7 @@ export function UserKodingInputMessage({
|
|
|
20
20
|
return (
|
|
21
21
|
<Box flexDirection="column" marginTop={addMargin ? 1 : 0} width="100%">
|
|
22
22
|
<Box>
|
|
23
|
-
<Text color={getTheme().
|
|
23
|
+
<Text color={getTheme().noting}>#</Text>
|
|
24
24
|
<Text color={getTheme().secondaryText}> {input}</Text>
|
|
25
25
|
</Box>
|
|
26
26
|
</Box>
|
package/src/entrypoints/cli.tsx
CHANGED
|
@@ -3,6 +3,23 @@ import { initSentry } from '../services/sentry'
|
|
|
3
3
|
import { PRODUCT_COMMAND, PRODUCT_NAME } from '../constants/product'
|
|
4
4
|
initSentry() // Initialize Sentry as early as possible
|
|
5
5
|
|
|
6
|
+
// Ensure YOGA_WASM_PATH is set for Ink across run modes (wrapper/dev)
|
|
7
|
+
// Resolve yoga.wasm relative to this file when missing using ESM-friendly APIs
|
|
8
|
+
try {
|
|
9
|
+
if (!process.env.YOGA_WASM_PATH) {
|
|
10
|
+
const { fileURLToPath } = await import('node:url')
|
|
11
|
+
const { dirname, join } = await import('node:path')
|
|
12
|
+
const { existsSync } = await import('node:fs')
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
14
|
+
const __dirname = dirname(__filename)
|
|
15
|
+
const devCandidate = join(__dirname, '../../yoga.wasm')
|
|
16
|
+
// Prefer dev path; wrapper already sets env for normal runs
|
|
17
|
+
process.env.YOGA_WASM_PATH = existsSync(devCandidate)
|
|
18
|
+
? devCandidate
|
|
19
|
+
: process.env.YOGA_WASM_PATH
|
|
20
|
+
}
|
|
21
|
+
} catch {}
|
|
22
|
+
|
|
6
23
|
// XXX: Without this line (and the Object.keys, even though it seems like it does nothing!),
|
|
7
24
|
// there is a bug in Bun only on Win32 that causes this import to be removed, even though
|
|
8
25
|
// its use is solely because of its side-effects.
|
|
@@ -74,8 +91,11 @@ import {
|
|
|
74
91
|
getLatestVersion,
|
|
75
92
|
installGlobalPackage,
|
|
76
93
|
assertMinVersion,
|
|
94
|
+
getUpdateCommandSuggestions,
|
|
77
95
|
} from '../utils/autoUpdater'
|
|
96
|
+
import { gt } from 'semver'
|
|
78
97
|
import { CACHE_PATHS } from '../utils/log'
|
|
98
|
+
// import { checkAndNotifyUpdate } from '../utils/autoUpdater'
|
|
79
99
|
import { PersistentShell } from '../utils/PersistentShell'
|
|
80
100
|
import { GATE_USE_EXTERNAL_UPDATER } from '../constants/betas'
|
|
81
101
|
import { clearTerminal } from '../utils/terminal'
|
|
@@ -290,6 +310,8 @@ async function main() {
|
|
|
290
310
|
}
|
|
291
311
|
}
|
|
292
312
|
|
|
313
|
+
// Disabled background notifier to avoid mid-screen logs during REPL
|
|
314
|
+
|
|
293
315
|
let inputPrompt = ''
|
|
294
316
|
let renderContext: RenderOptions | undefined = {
|
|
295
317
|
exitOnCtrlC: false,
|
|
@@ -417,6 +439,18 @@ ${commandList}`,
|
|
|
417
439
|
} else {
|
|
418
440
|
const isDefaultModel = await isDefaultSlowAndCapableModel()
|
|
419
441
|
|
|
442
|
+
// Prefetch update info before first render to place banner at top
|
|
443
|
+
const updateInfo = await (async () => {
|
|
444
|
+
try {
|
|
445
|
+
const latest = await getLatestVersion()
|
|
446
|
+
if (latest && gt(latest, MACRO.VERSION)) {
|
|
447
|
+
const cmds = await getUpdateCommandSuggestions()
|
|
448
|
+
return { version: latest as string, commands: cmds as string[] }
|
|
449
|
+
}
|
|
450
|
+
} catch {}
|
|
451
|
+
return { version: null as string | null, commands: null as string[] | null }
|
|
452
|
+
})()
|
|
453
|
+
|
|
420
454
|
render(
|
|
421
455
|
<REPL
|
|
422
456
|
commands={commands}
|
|
@@ -429,6 +463,8 @@ ${commandList}`,
|
|
|
429
463
|
safeMode={safe}
|
|
430
464
|
mcpClients={mcpClients}
|
|
431
465
|
isDefaultModel={isDefaultModel}
|
|
466
|
+
initialUpdateVersion={updateInfo.version}
|
|
467
|
+
initialUpdateCommands={updateInfo.commands}
|
|
432
468
|
/>,
|
|
433
469
|
renderContext,
|
|
434
470
|
)
|
|
@@ -1098,11 +1134,11 @@ ${commandList}`,
|
|
|
1098
1134
|
<Box
|
|
1099
1135
|
flexDirection="column"
|
|
1100
1136
|
borderStyle="round"
|
|
1101
|
-
|
|
1137
|
+
borderColor={theme.kode}
|
|
1102
1138
|
padding={1}
|
|
1103
1139
|
width={'100%'}
|
|
1104
1140
|
>
|
|
1105
|
-
<Text bold color={theme.
|
|
1141
|
+
<Text bold color={theme.kode}>
|
|
1106
1142
|
Import MCP Servers from Claude Desktop
|
|
1107
1143
|
</Text>
|
|
1108
1144
|
|
|
@@ -1219,16 +1255,8 @@ ${commandList}`,
|
|
|
1219
1255
|
// claude update
|
|
1220
1256
|
program
|
|
1221
1257
|
.command('update')
|
|
1222
|
-
.description('
|
|
1258
|
+
.description('Show manual upgrade commands (no auto-install)')
|
|
1223
1259
|
.action(async () => {
|
|
1224
|
-
const useExternalUpdater = await checkGate(GATE_USE_EXTERNAL_UPDATER)
|
|
1225
|
-
if (useExternalUpdater) {
|
|
1226
|
-
// The external updater intercepts calls to "claude update", which means if we have received
|
|
1227
|
-
// this command at all, the extenral updater isn't installed on this machine.
|
|
1228
|
-
console.log(`This version of ${PRODUCT_NAME} is no longer supported.`)
|
|
1229
|
-
process.exit(0)
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
1260
|
logEvent('tengu_update_check', {})
|
|
1233
1261
|
console.log(`Current version: ${MACRO.VERSION}`)
|
|
1234
1262
|
console.log('Checking for updates...')
|
|
@@ -1246,30 +1274,12 @@ ${commandList}`,
|
|
|
1246
1274
|
}
|
|
1247
1275
|
|
|
1248
1276
|
console.log(`New version available: ${latestVersion}`)
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
console.log(`Successfully updated to version ${latestVersion}`)
|
|
1256
|
-
break
|
|
1257
|
-
case 'no_permissions':
|
|
1258
|
-
console.error('Error: Insufficient permissions to install update')
|
|
1259
|
-
console.error('Try running with sudo or fix npm permissions')
|
|
1260
|
-
process.exit(1)
|
|
1261
|
-
break
|
|
1262
|
-
case 'install_failed':
|
|
1263
|
-
console.error('Error: Failed to install update')
|
|
1264
|
-
process.exit(1)
|
|
1265
|
-
break
|
|
1266
|
-
case 'in_progress':
|
|
1267
|
-
console.error(
|
|
1268
|
-
'Error: Another instance is currently performing an update',
|
|
1269
|
-
)
|
|
1270
|
-
console.error('Please wait and try again later')
|
|
1271
|
-
process.exit(1)
|
|
1272
|
-
break
|
|
1277
|
+
const { getUpdateCommandSuggestions } = await import('../utils/autoUpdater')
|
|
1278
|
+
const cmds = await getUpdateCommandSuggestions()
|
|
1279
|
+
console.log('\nRun one of the following commands to update:')
|
|
1280
|
+
for (const c of cmds) console.log(` ${c}`)
|
|
1281
|
+
if (process.platform !== 'win32') {
|
|
1282
|
+
console.log('\nNote: you may need to prefix with "sudo" on macOS/Linux.')
|
|
1273
1283
|
}
|
|
1274
1284
|
process.exit(0)
|
|
1275
1285
|
})
|
|
@@ -1501,9 +1511,23 @@ process.on('exit', () => {
|
|
|
1501
1511
|
PersistentShell.getInstance().close()
|
|
1502
1512
|
})
|
|
1503
1513
|
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1514
|
+
function gracefulExit(code = 0) {
|
|
1515
|
+
try { resetCursor() } catch {}
|
|
1516
|
+
try { PersistentShell.getInstance().close() } catch {}
|
|
1517
|
+
process.exit(code)
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
process.on('SIGINT', () => gracefulExit(0))
|
|
1521
|
+
process.on('SIGTERM', () => gracefulExit(0))
|
|
1522
|
+
// Windows CTRL+BREAK
|
|
1523
|
+
process.on('SIGBREAK', () => gracefulExit(0))
|
|
1524
|
+
process.on('unhandledRejection', err => {
|
|
1525
|
+
console.error('Unhandled rejection:', err)
|
|
1526
|
+
gracefulExit(1)
|
|
1527
|
+
})
|
|
1528
|
+
process.on('uncaughtException', err => {
|
|
1529
|
+
console.error('Uncaught exception:', err)
|
|
1530
|
+
gracefulExit(1)
|
|
1507
1531
|
})
|
|
1508
1532
|
|
|
1509
1533
|
function resetCursor() {
|
package/src/screens/REPL.tsx
CHANGED
|
@@ -44,6 +44,7 @@ import type { WrappedClient } from '../services/mcpClient'
|
|
|
44
44
|
import type { Tool } from '../Tool'
|
|
45
45
|
import { AutoUpdaterResult } from '../utils/autoUpdater'
|
|
46
46
|
import { getGlobalConfig, saveGlobalConfig } from '../utils/config'
|
|
47
|
+
import { MACRO } from '../constants/macros'
|
|
47
48
|
import { logEvent } from '../services/statsig'
|
|
48
49
|
import { getNextAvailableLogForkNumber } from '../utils/log'
|
|
49
50
|
import {
|
|
@@ -87,6 +88,9 @@ type Props = {
|
|
|
87
88
|
mcpClients?: WrappedClient[]
|
|
88
89
|
// Flag to indicate if current model is default
|
|
89
90
|
isDefaultModel?: boolean
|
|
91
|
+
// Update banner info passed from CLI before first render
|
|
92
|
+
initialUpdateVersion?: string | null
|
|
93
|
+
initialUpdateCommands?: string[] | null
|
|
90
94
|
}
|
|
91
95
|
|
|
92
96
|
export type BinaryFeedbackContext = {
|
|
@@ -108,6 +112,8 @@ export function REPL({
|
|
|
108
112
|
initialMessages,
|
|
109
113
|
mcpClients = [],
|
|
110
114
|
isDefaultModel = true,
|
|
115
|
+
initialUpdateVersion,
|
|
116
|
+
initialUpdateCommands,
|
|
111
117
|
}: Props): React.ReactNode {
|
|
112
118
|
// TODO: probably shouldn't re-read config from file synchronously on every keystroke
|
|
113
119
|
const verbose = verboseFromCLI ?? getGlobalConfig().verbose
|
|
@@ -149,6 +155,10 @@ export function REPL({
|
|
|
149
155
|
|
|
150
156
|
const [binaryFeedbackContext, setBinaryFeedbackContext] =
|
|
151
157
|
useState<BinaryFeedbackContext | null>(null)
|
|
158
|
+
// New version banner: passed in from CLI to guarantee top placement
|
|
159
|
+
const updateAvailableVersion = initialUpdateVersion ?? null
|
|
160
|
+
const updateCommands = initialUpdateCommands ?? null
|
|
161
|
+
// No separate Static for banner; it renders inside Logo
|
|
152
162
|
|
|
153
163
|
const getBinaryFeedbackResponse = useCallback(
|
|
154
164
|
(
|
|
@@ -209,6 +219,8 @@ export function REPL({
|
|
|
209
219
|
}
|
|
210
220
|
}, [messages, showCostDialog, haveShownCostDialog])
|
|
211
221
|
|
|
222
|
+
// Update banner is provided by CLI at startup; no async check here.
|
|
223
|
+
|
|
212
224
|
const canUseTool = useCanUseTool(setToolUseConfirm)
|
|
213
225
|
|
|
214
226
|
async function onInit() {
|
|
@@ -478,7 +490,12 @@ export function REPL({
|
|
|
478
490
|
type: 'static',
|
|
479
491
|
jsx: (
|
|
480
492
|
<Box flexDirection="column" key={`logo${forkNumber}`}>
|
|
481
|
-
<Logo
|
|
493
|
+
<Logo
|
|
494
|
+
mcpClients={mcpClients}
|
|
495
|
+
isDefaultModel={isDefaultModel}
|
|
496
|
+
updateBannerVersion={updateAvailableVersion}
|
|
497
|
+
updateBannerCommands={updateCommands}
|
|
498
|
+
/>
|
|
482
499
|
<ProjectOnboarding workspaceDir={getOriginalCwd()} />
|
|
483
500
|
</Box>
|
|
484
501
|
),
|
|
@@ -602,6 +619,7 @@ export function REPL({
|
|
|
602
619
|
|
|
603
620
|
return (
|
|
604
621
|
<PermissionProvider isBypassPermissionsModeAvailable={!safeMode}>
|
|
622
|
+
{/* Update banner now renders inside Logo for stable placement */}
|
|
605
623
|
<ModeIndicator />
|
|
606
624
|
<React.Fragment key={`static-messages-${forkNumber}`}>
|
|
607
625
|
<Static
|
package/src/services/openai.ts
CHANGED
|
@@ -518,7 +518,7 @@ export async function getCompletionWithProfile(
|
|
|
518
518
|
messageCount: opts.messages?.length || 0,
|
|
519
519
|
streamMode: opts.stream,
|
|
520
520
|
timestamp: new Date().toISOString(),
|
|
521
|
-
|
|
521
|
+
modelProfileModelName: modelProfile?.modelName,
|
|
522
522
|
modelProfileName: modelProfile?.name,
|
|
523
523
|
})
|
|
524
524
|
|
package/src/services/statsig.ts
CHANGED
|
@@ -107,7 +107,8 @@ export function logEvent(
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
export const checkGate = memoize(async (gateName: string): Promise<boolean> => {
|
|
110
|
-
|
|
110
|
+
// Default to disabled gates when Statsig is not active
|
|
111
|
+
return false
|
|
111
112
|
// if (env.isCI || process.env.NODE_ENV === 'test') {
|
|
112
113
|
// return false
|
|
113
114
|
// }
|
|
@@ -120,7 +121,7 @@ export const checkGate = memoize(async (gateName: string): Promise<boolean> => {
|
|
|
120
121
|
})
|
|
121
122
|
|
|
122
123
|
export const useStatsigGate = (gateName: string, defaultValue = false) => {
|
|
123
|
-
return
|
|
124
|
+
return false
|
|
124
125
|
// const [gateValue, setGateValue] = React.useState(defaultValue)
|
|
125
126
|
// React.useEffect(() => {
|
|
126
127
|
// checkGate(gateName).then(setGateValue)
|
package/src/utils/agentLoader.ts
CHANGED
|
@@ -263,22 +263,16 @@ export async function startAgentWatcher(onChange?: () => void): Promise<void> {
|
|
|
263
263
|
* Stop watching agent configuration directories
|
|
264
264
|
*/
|
|
265
265
|
export async function stopAgentWatcher(): Promise<void> {
|
|
266
|
-
|
|
267
|
-
|
|
266
|
+
// FSWatcher.close() is synchronous and does not accept a callback on Node 18/20
|
|
267
|
+
try {
|
|
268
|
+
for (const watcher of watchers) {
|
|
268
269
|
try {
|
|
269
|
-
watcher.close(
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
}
|
|
273
|
-
resolve()
|
|
274
|
-
})
|
|
275
|
-
} catch (error) {
|
|
276
|
-
console.error('Error closing watcher:', error)
|
|
277
|
-
resolve()
|
|
270
|
+
watcher.close()
|
|
271
|
+
} catch (err) {
|
|
272
|
+
console.error('Failed to close file watcher:', err)
|
|
278
273
|
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
}
|
|
274
|
+
}
|
|
275
|
+
} finally {
|
|
276
|
+
watchers = []
|
|
277
|
+
}
|
|
278
|
+
}
|
package/src/utils/autoUpdater.ts
CHANGED
|
@@ -12,13 +12,16 @@ import {
|
|
|
12
12
|
} from 'fs'
|
|
13
13
|
import { platform } from 'process'
|
|
14
14
|
import { execFileNoThrow } from './execFileNoThrow'
|
|
15
|
+
import { spawn } from 'child_process'
|
|
15
16
|
import { logError } from './log'
|
|
16
17
|
import { accessSync } from 'fs'
|
|
17
18
|
import { CLAUDE_BASE_DIR } from './env'
|
|
18
19
|
import { logEvent, getDynamicConfig } from '../services/statsig'
|
|
19
|
-
import { lt } from 'semver'
|
|
20
|
+
import { lt, gt } from 'semver'
|
|
20
21
|
import { MACRO } from '../constants/macros'
|
|
21
22
|
import { PRODUCT_COMMAND, PRODUCT_NAME } from '../constants/product'
|
|
23
|
+
import { getGlobalConfig, saveGlobalConfig, isAutoUpdaterDisabled } from './config'
|
|
24
|
+
import { env } from './env'
|
|
22
25
|
export type InstallStatus =
|
|
23
26
|
| 'success'
|
|
24
27
|
| 'no_permissions'
|
|
@@ -49,14 +52,12 @@ export async function assertMinVersion(): Promise<void> {
|
|
|
49
52
|
versionConfig.minVersion &&
|
|
50
53
|
lt(MACRO.VERSION, versionConfig.minVersion)
|
|
51
54
|
) {
|
|
55
|
+
const suggestions = await getUpdateCommandSuggestions()
|
|
56
|
+
const cmdLines = suggestions.map(c => ` ${c}`).join('\n')
|
|
52
57
|
console.error(`
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
To update, please run:
|
|
57
|
-
${PRODUCT_COMMAND} update
|
|
58
|
-
|
|
59
|
-
This will ensure you have access to the latest features and improvements.
|
|
58
|
+
您的 ${PRODUCT_NAME} 版本 (${MACRO.VERSION}) 过低,需要升级到 ${versionConfig.minVersion} 或更高版本。
|
|
59
|
+
请手动执行以下任一命令进行升级:
|
|
60
|
+
${cmdLines}
|
|
60
61
|
`)
|
|
61
62
|
process.exit(1)
|
|
62
63
|
}
|
|
@@ -267,21 +268,48 @@ export function getPermissionsCommand(npmPrefix: string): string {
|
|
|
267
268
|
}
|
|
268
269
|
|
|
269
270
|
export async function getLatestVersion(): Promise<string | null> {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
271
|
+
// 1) Try npm CLI (fast when available)
|
|
272
|
+
try {
|
|
273
|
+
const abortController = new AbortController()
|
|
274
|
+
setTimeout(() => abortController.abort(), 5000)
|
|
275
|
+
const result = await execFileNoThrow(
|
|
276
|
+
'npm',
|
|
277
|
+
['view', MACRO.PACKAGE_URL, 'version'],
|
|
278
|
+
abortController.signal,
|
|
279
|
+
)
|
|
280
|
+
if (result.code === 0) {
|
|
281
|
+
const v = result.stdout.trim()
|
|
282
|
+
if (v) return v
|
|
283
|
+
}
|
|
284
|
+
} catch {}
|
|
285
|
+
|
|
286
|
+
// 2) Fallback: fetch npm registry (works in Bun/Node without npm)
|
|
287
|
+
try {
|
|
288
|
+
const controller = new AbortController()
|
|
289
|
+
const timer = setTimeout(() => controller.abort(), 5000)
|
|
290
|
+
const res = await fetch(
|
|
291
|
+
`https://registry.npmjs.org/${encodeURIComponent(MACRO.PACKAGE_URL)}`,
|
|
292
|
+
{
|
|
293
|
+
method: 'GET',
|
|
294
|
+
headers: {
|
|
295
|
+
Accept: 'application/vnd.npm.install-v1+json',
|
|
296
|
+
'User-Agent': `${PRODUCT_NAME}/${MACRO.VERSION}`,
|
|
297
|
+
},
|
|
298
|
+
signal: controller.signal,
|
|
299
|
+
},
|
|
300
|
+
)
|
|
301
|
+
clearTimeout(timer)
|
|
302
|
+
if (!res.ok) return null
|
|
303
|
+
const json: any = await res.json().catch(() => null)
|
|
304
|
+
const latest = json && json['dist-tags'] && json['dist-tags'].latest
|
|
305
|
+
return typeof latest === 'string' ? latest : null
|
|
306
|
+
} catch {
|
|
279
307
|
return null
|
|
280
308
|
}
|
|
281
|
-
return result.stdout.trim()
|
|
282
309
|
}
|
|
283
310
|
|
|
284
311
|
export async function installGlobalPackage(): Promise<InstallStatus> {
|
|
312
|
+
// Detect preferred package manager and install accordingly
|
|
285
313
|
if (!acquireLock()) {
|
|
286
314
|
logError('Another process is currently installing an update')
|
|
287
315
|
// Log the lock contention to statsig
|
|
@@ -293,26 +321,138 @@ export async function installGlobalPackage(): Promise<InstallStatus> {
|
|
|
293
321
|
}
|
|
294
322
|
|
|
295
323
|
try {
|
|
296
|
-
const
|
|
297
|
-
if (
|
|
298
|
-
|
|
324
|
+
const manager = await detectPackageManager()
|
|
325
|
+
if (manager === 'npm') {
|
|
326
|
+
const { hasPermissions } = await checkNpmPermissions()
|
|
327
|
+
if (!hasPermissions) {
|
|
328
|
+
return 'no_permissions'
|
|
329
|
+
}
|
|
330
|
+
// Stream实时输出,减少用户等待感
|
|
331
|
+
const code = await runStreaming('npm', ['install', '-g', MACRO.PACKAGE_URL])
|
|
332
|
+
if (code !== 0) {
|
|
333
|
+
logError(`Failed to install new version via npm (exit ${code})`)
|
|
334
|
+
return 'install_failed'
|
|
335
|
+
}
|
|
336
|
+
return 'success'
|
|
299
337
|
}
|
|
300
338
|
|
|
301
|
-
|
|
302
|
-
'
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
`Failed to install new version of claude: ${installResult.stdout} ${installResult.stderr}`,
|
|
309
|
-
)
|
|
310
|
-
return 'install_failed'
|
|
339
|
+
if (manager === 'bun') {
|
|
340
|
+
const code = await runStreaming('bun', ['add', '-g', `${MACRO.PACKAGE_URL}@latest`])
|
|
341
|
+
if (code !== 0) {
|
|
342
|
+
logError(`Failed to install new version via bun (exit ${code})`)
|
|
343
|
+
return 'install_failed'
|
|
344
|
+
}
|
|
345
|
+
return 'success'
|
|
311
346
|
}
|
|
312
347
|
|
|
348
|
+
// Fallback to npm if unknown
|
|
349
|
+
const { hasPermissions } = await checkNpmPermissions()
|
|
350
|
+
if (!hasPermissions) return 'no_permissions'
|
|
351
|
+
const code = await runStreaming('npm', ['install', '-g', MACRO.PACKAGE_URL])
|
|
352
|
+
if (code !== 0) return 'install_failed'
|
|
313
353
|
return 'success'
|
|
314
354
|
} finally {
|
|
315
355
|
// Ensure we always release the lock
|
|
316
356
|
releaseLock()
|
|
317
357
|
}
|
|
318
358
|
}
|
|
359
|
+
|
|
360
|
+
export type PackageManager = 'npm' | 'bun'
|
|
361
|
+
|
|
362
|
+
export async function detectPackageManager(): Promise<PackageManager> {
|
|
363
|
+
// Respect explicit override if provided later via config/env (future-proof)
|
|
364
|
+
try {
|
|
365
|
+
// Heuristic 1: npm available and global root resolvable
|
|
366
|
+
const npmRoot = await execFileNoThrow('npm', ['-g', 'root'])
|
|
367
|
+
if (npmRoot.code === 0 && npmRoot.stdout.trim()) {
|
|
368
|
+
return 'npm'
|
|
369
|
+
}
|
|
370
|
+
} catch {}
|
|
371
|
+
|
|
372
|
+
try {
|
|
373
|
+
// Heuristic 2: running on a system with bun and installed path hints bun
|
|
374
|
+
const bunVer = await execFileNoThrow('bun', ['--version'])
|
|
375
|
+
if (bunVer.code === 0) {
|
|
376
|
+
// BUN_INSTALL defaults to ~/.bun; if our package lives under that tree, prefer bun
|
|
377
|
+
// If npm not detected but bun is available, choose bun
|
|
378
|
+
return 'bun'
|
|
379
|
+
}
|
|
380
|
+
} catch {}
|
|
381
|
+
|
|
382
|
+
// Default to npm when uncertain
|
|
383
|
+
return 'npm'
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function runStreaming(cmd: string, args: string[]): Promise<number> {
|
|
387
|
+
return new Promise(resolve => {
|
|
388
|
+
// 打印正在使用的包管理器与命令,增强透明度
|
|
389
|
+
try {
|
|
390
|
+
// eslint-disable-next-line no-console
|
|
391
|
+
console.log(`> ${cmd} ${args.join(' ')}`)
|
|
392
|
+
} catch {}
|
|
393
|
+
|
|
394
|
+
const child = spawn(cmd, args, {
|
|
395
|
+
stdio: 'inherit',
|
|
396
|
+
env: process.env,
|
|
397
|
+
})
|
|
398
|
+
child.on('close', code => resolve(code ?? 0))
|
|
399
|
+
child.on('error', () => resolve(1))
|
|
400
|
+
})
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Generate human-friendly update commands for the detected package manager.
|
|
405
|
+
* Also includes an alternative manager command as fallback for users.
|
|
406
|
+
*/
|
|
407
|
+
export async function getUpdateCommandSuggestions(): Promise<string[]> {
|
|
408
|
+
// Prefer Bun first, then npm (consistent, simple UX). Include @latest.
|
|
409
|
+
return [
|
|
410
|
+
`bun add -g ${MACRO.PACKAGE_URL}@latest`,
|
|
411
|
+
`npm install -g ${MACRO.PACKAGE_URL}@latest`,
|
|
412
|
+
]
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Non-blocking update notifier (daily)
|
|
417
|
+
* - Respects CI and disabled auto-updater
|
|
418
|
+
* - Uses env.hasInternetAccess() to quickly skip offline cases
|
|
419
|
+
* - Stores last check timestamp + last suggested version in global config
|
|
420
|
+
*/
|
|
421
|
+
export async function checkAndNotifyUpdate(): Promise<void> {
|
|
422
|
+
try {
|
|
423
|
+
if (process.env.NODE_ENV === 'test') return
|
|
424
|
+
if (await isAutoUpdaterDisabled()) return
|
|
425
|
+
if (await env.getIsDocker()) return
|
|
426
|
+
if (!(await env.hasInternetAccess())) return
|
|
427
|
+
|
|
428
|
+
const config: any = getGlobalConfig()
|
|
429
|
+
const now = Date.now()
|
|
430
|
+
const DAY_MS = 24 * 60 * 60 * 1000
|
|
431
|
+
const lastCheck = Number(config.lastUpdateCheckAt || 0)
|
|
432
|
+
if (lastCheck && now - lastCheck < DAY_MS) return
|
|
433
|
+
|
|
434
|
+
const latest = await getLatestVersion()
|
|
435
|
+
if (!latest) {
|
|
436
|
+
// Still record the check to avoid spamming
|
|
437
|
+
saveGlobalConfig({ ...config, lastUpdateCheckAt: now })
|
|
438
|
+
return
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (gt(latest, MACRO.VERSION)) {
|
|
442
|
+
// Update stored state and print a low-noise hint
|
|
443
|
+
saveGlobalConfig({
|
|
444
|
+
...config,
|
|
445
|
+
lastUpdateCheckAt: now,
|
|
446
|
+
lastSuggestedVersion: latest,
|
|
447
|
+
})
|
|
448
|
+
const suggestions = await getUpdateCommandSuggestions()
|
|
449
|
+
const first = suggestions[0]
|
|
450
|
+
console.log(`New version available: ${latest}. Recommended: ${first}`)
|
|
451
|
+
} else {
|
|
452
|
+
saveGlobalConfig({ ...config, lastUpdateCheckAt: now })
|
|
453
|
+
}
|
|
454
|
+
} catch (error) {
|
|
455
|
+
// Never block or throw; just log and move on
|
|
456
|
+
logError(`update-notify: ${error}`)
|
|
457
|
+
}
|
|
458
|
+
}
|
package/src/utils/config.ts
CHANGED
|
@@ -173,6 +173,8 @@ export type GlobalConfig = {
|
|
|
173
173
|
modelProfiles?: ModelProfile[] // Model configuration list
|
|
174
174
|
modelPointers?: ModelPointers // Model pointer system
|
|
175
175
|
defaultModelName?: string // Default model
|
|
176
|
+
// Update notifications
|
|
177
|
+
lastDismissedUpdateVersion?: string
|
|
176
178
|
}
|
|
177
179
|
|
|
178
180
|
export const DEFAULT_GLOBAL_CONFIG: GlobalConfig = {
|
|
@@ -196,6 +198,7 @@ export const DEFAULT_GLOBAL_CONFIG: GlobalConfig = {
|
|
|
196
198
|
reasoning: '',
|
|
197
199
|
quick: '',
|
|
198
200
|
},
|
|
201
|
+
lastDismissedUpdateVersion: undefined,
|
|
199
202
|
}
|
|
200
203
|
|
|
201
204
|
export const GLOBAL_CONFIG_KEYS = [
|
package/src/utils/file.ts
CHANGED
|
@@ -98,12 +98,15 @@ export function isInDirectory(
|
|
|
98
98
|
: normalizedCwd + sep
|
|
99
99
|
|
|
100
100
|
// Join with a base directory to make them absolute-like for comparison
|
|
101
|
-
// Using 'dummy' as base to avoid any actual file system dependencies
|
|
102
101
|
const fullPath = resolvePath(cwd(), normalizedCwd, normalizedPath)
|
|
103
102
|
const fullCwd = resolvePath(cwd(), normalizedCwd)
|
|
104
103
|
|
|
105
|
-
//
|
|
106
|
-
|
|
104
|
+
// Robust subpath check using path.relative (case-insensitive on Windows)
|
|
105
|
+
const rel = relative(fullCwd, fullPath)
|
|
106
|
+
if (!rel || rel === '') return true
|
|
107
|
+
if (rel.startsWith('..')) return false
|
|
108
|
+
if (isAbsolute(rel)) return false
|
|
109
|
+
return true
|
|
107
110
|
}
|
|
108
111
|
|
|
109
112
|
export function readTextContent(
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isAbsolute, resolve } from 'path'
|
|
1
|
+
import { isAbsolute, resolve, relative, sep } from 'path'
|
|
2
2
|
import { getCwd, getOriginalCwd } from '../state'
|
|
3
3
|
|
|
4
4
|
// In-memory storage for file permissions that resets each session
|
|
@@ -12,7 +12,26 @@ const writeFileAllowedDirectories: Set<string> = new Set()
|
|
|
12
12
|
* @returns Absolute path
|
|
13
13
|
*/
|
|
14
14
|
export function toAbsolutePath(path: string): string {
|
|
15
|
-
|
|
15
|
+
const abs = isAbsolute(path) ? resolve(path) : resolve(getCwd(), path)
|
|
16
|
+
return normalizeForCompare(abs)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function normalizeForCompare(p: string): string {
|
|
20
|
+
// Normalize separators and resolve .. and . segments
|
|
21
|
+
const norm = resolve(p)
|
|
22
|
+
// On Windows, comparisons should be case-insensitive
|
|
23
|
+
return process.platform === 'win32' ? norm.toLowerCase() : norm
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isSubpath(base: string, target: string): boolean {
|
|
27
|
+
const rel = relative(base, target)
|
|
28
|
+
// If different drive letters on Windows, relative returns the target path
|
|
29
|
+
if (!rel || rel === '') return true
|
|
30
|
+
// Not a subpath if it goes up to parent
|
|
31
|
+
if (rel.startsWith('..')) return false
|
|
32
|
+
// Not a subpath if absolute
|
|
33
|
+
if (isAbsolute(rel)) return false
|
|
34
|
+
return true
|
|
16
35
|
}
|
|
17
36
|
|
|
18
37
|
/**
|
|
@@ -22,7 +41,8 @@ export function toAbsolutePath(path: string): string {
|
|
|
22
41
|
*/
|
|
23
42
|
export function pathInOriginalCwd(path: string): boolean {
|
|
24
43
|
const absolutePath = toAbsolutePath(path)
|
|
25
|
-
|
|
44
|
+
const base = toAbsolutePath(getOriginalCwd())
|
|
45
|
+
return isSubpath(base, absolutePath)
|
|
26
46
|
}
|
|
27
47
|
|
|
28
48
|
/**
|
|
@@ -32,12 +52,8 @@ export function pathInOriginalCwd(path: string): boolean {
|
|
|
32
52
|
*/
|
|
33
53
|
export function hasReadPermission(directory: string): boolean {
|
|
34
54
|
const absolutePath = toAbsolutePath(directory)
|
|
35
|
-
|
|
36
55
|
for (const allowedPath of readFileAllowedDirectories) {
|
|
37
|
-
|
|
38
|
-
if (absolutePath.startsWith(allowedPath)) {
|
|
39
|
-
return true
|
|
40
|
-
}
|
|
56
|
+
if (isSubpath(allowedPath, absolutePath)) return true
|
|
41
57
|
}
|
|
42
58
|
return false
|
|
43
59
|
}
|
|
@@ -49,12 +65,8 @@ export function hasReadPermission(directory: string): boolean {
|
|
|
49
65
|
*/
|
|
50
66
|
export function hasWritePermission(directory: string): boolean {
|
|
51
67
|
const absolutePath = toAbsolutePath(directory)
|
|
52
|
-
|
|
53
68
|
for (const allowedPath of writeFileAllowedDirectories) {
|
|
54
|
-
|
|
55
|
-
if (absolutePath.startsWith(allowedPath)) {
|
|
56
|
-
return true
|
|
57
|
-
}
|
|
69
|
+
if (isSubpath(allowedPath, absolutePath)) return true
|
|
58
70
|
}
|
|
59
71
|
return false
|
|
60
72
|
}
|
|
@@ -65,10 +77,9 @@ export function hasWritePermission(directory: string): boolean {
|
|
|
65
77
|
*/
|
|
66
78
|
function saveReadPermission(directory: string): void {
|
|
67
79
|
const absolutePath = toAbsolutePath(directory)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (allowedPath.startsWith(absolutePath)) {
|
|
80
|
+
// Remove any existing subpaths contained by this new path
|
|
81
|
+
for (const allowedPath of Array.from(readFileAllowedDirectories)) {
|
|
82
|
+
if (isSubpath(absolutePath, allowedPath)) {
|
|
72
83
|
readFileAllowedDirectories.delete(allowedPath)
|
|
73
84
|
}
|
|
74
85
|
}
|
|
@@ -92,10 +103,8 @@ export function grantReadPermissionForOriginalDir(): void {
|
|
|
92
103
|
*/
|
|
93
104
|
function saveWritePermission(directory: string): void {
|
|
94
105
|
const absolutePath = toAbsolutePath(directory)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
for (const allowedPath of writeFileAllowedDirectories) {
|
|
98
|
-
if (allowedPath.startsWith(absolutePath)) {
|
|
106
|
+
for (const allowedPath of Array.from(writeFileAllowedDirectories)) {
|
|
107
|
+
if (isSubpath(absolutePath, allowedPath)) {
|
|
99
108
|
writeFileAllowedDirectories.delete(allowedPath)
|
|
100
109
|
}
|
|
101
110
|
}
|
package/src/utils/secureFile.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync, unlinkSync, renameSync } from 'node:fs'
|
|
2
|
-
import { join, dirname, normalize, resolve, extname } from 'node:path'
|
|
2
|
+
import { join, dirname, normalize, resolve, extname, relative, isAbsolute } from 'node:path'
|
|
3
3
|
import { homedir } from 'node:os'
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -98,7 +98,12 @@ export class SecureFileService {
|
|
|
98
98
|
|
|
99
99
|
// 检查是否在允许的基础路径中
|
|
100
100
|
const isInAllowedPath = Array.from(this.allowedBasePaths).some(basePath => {
|
|
101
|
-
|
|
101
|
+
const base = resolve(basePath)
|
|
102
|
+
const rel = relative(base, absolutePath)
|
|
103
|
+
if (!rel || rel === '') return true
|
|
104
|
+
if (rel.startsWith('..')) return false
|
|
105
|
+
if (isAbsolute(rel)) return false
|
|
106
|
+
return true
|
|
102
107
|
})
|
|
103
108
|
|
|
104
109
|
if (!isInAllowedPath) {
|
|
@@ -556,4 +561,4 @@ export class SecureFileService {
|
|
|
556
561
|
}
|
|
557
562
|
|
|
558
563
|
// 导出单例实例
|
|
559
|
-
export const secureFileService = SecureFileService.getInstance()
|
|
564
|
+
export const secureFileService = SecureFileService.getInstance()
|
package/src/utils/theme.ts
CHANGED
|
@@ -2,18 +2,16 @@ import { getGlobalConfig } from './config'
|
|
|
2
2
|
|
|
3
3
|
export interface Theme {
|
|
4
4
|
bashBorder: string
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
kode: string
|
|
6
|
+
noting: string
|
|
7
7
|
permission: string
|
|
8
8
|
secondaryBorder: string
|
|
9
9
|
text: string
|
|
10
10
|
secondaryText: string
|
|
11
11
|
suggestion: string
|
|
12
|
-
// Semantic colors
|
|
13
12
|
success: string
|
|
14
13
|
error: string
|
|
15
14
|
warning: string
|
|
16
|
-
// UI colors
|
|
17
15
|
primary: string
|
|
18
16
|
secondary: string
|
|
19
17
|
diff: {
|
|
@@ -25,9 +23,9 @@ export interface Theme {
|
|
|
25
23
|
}
|
|
26
24
|
|
|
27
25
|
const lightTheme: Theme = {
|
|
28
|
-
bashBorder: '#
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
bashBorder: '#FF6E57',
|
|
27
|
+
kode: '#FFC233',
|
|
28
|
+
noting: '#222222',
|
|
31
29
|
permission: '#e9c61aff',
|
|
32
30
|
secondaryBorder: '#999',
|
|
33
31
|
text: '#000',
|
|
@@ -47,31 +45,31 @@ const lightTheme: Theme = {
|
|
|
47
45
|
}
|
|
48
46
|
|
|
49
47
|
const lightDaltonizedTheme: Theme = {
|
|
50
|
-
bashBorder: '#
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
permission: '#3366ff',
|
|
48
|
+
bashBorder: '#FF6E57',
|
|
49
|
+
kode: '#FFC233',
|
|
50
|
+
noting: '#222222',
|
|
51
|
+
permission: '#3366ff',
|
|
54
52
|
secondaryBorder: '#999',
|
|
55
53
|
text: '#000',
|
|
56
54
|
secondaryText: '#666',
|
|
57
55
|
suggestion: '#3366ff',
|
|
58
|
-
success: '#006699',
|
|
59
|
-
error: '#cc0000',
|
|
60
|
-
warning: '#ff9900',
|
|
56
|
+
success: '#006699',
|
|
57
|
+
error: '#cc0000',
|
|
58
|
+
warning: '#ff9900',
|
|
61
59
|
primary: '#000',
|
|
62
60
|
secondary: '#666',
|
|
63
61
|
diff: {
|
|
64
|
-
added: '#99ccff',
|
|
65
|
-
removed: '#ffcccc',
|
|
62
|
+
added: '#99ccff',
|
|
63
|
+
removed: '#ffcccc',
|
|
66
64
|
addedDimmed: '#d1e7fd',
|
|
67
65
|
removedDimmed: '#ffe9e9',
|
|
68
66
|
},
|
|
69
67
|
}
|
|
70
68
|
|
|
71
69
|
const darkTheme: Theme = {
|
|
72
|
-
bashBorder: '#
|
|
73
|
-
|
|
74
|
-
|
|
70
|
+
bashBorder: '#FF6E57',
|
|
71
|
+
kode: '#FFC233',
|
|
72
|
+
noting: '#222222',
|
|
75
73
|
permission: '#b1b9f9',
|
|
76
74
|
secondaryBorder: '#888',
|
|
77
75
|
text: '#fff',
|
|
@@ -91,32 +89,28 @@ const darkTheme: Theme = {
|
|
|
91
89
|
}
|
|
92
90
|
|
|
93
91
|
const darkDaltonizedTheme: Theme = {
|
|
94
|
-
bashBorder: '#
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
permission: '#99ccff',
|
|
92
|
+
bashBorder: '#FF6E57',
|
|
93
|
+
kode: '#FFC233',
|
|
94
|
+
noting: '#222222',
|
|
95
|
+
permission: '#99ccff',
|
|
98
96
|
secondaryBorder: '#888',
|
|
99
97
|
text: '#fff',
|
|
100
98
|
secondaryText: '#999',
|
|
101
99
|
suggestion: '#99ccff',
|
|
102
|
-
success: '#3399ff',
|
|
103
|
-
error: '#ff6666',
|
|
104
|
-
warning: '#ffcc00',
|
|
100
|
+
success: '#3399ff',
|
|
101
|
+
error: '#ff6666',
|
|
102
|
+
warning: '#ffcc00',
|
|
105
103
|
primary: '#fff',
|
|
106
104
|
secondary: '#999',
|
|
107
105
|
diff: {
|
|
108
|
-
added: '#004466',
|
|
109
|
-
removed: '#660000',
|
|
106
|
+
added: '#004466',
|
|
107
|
+
removed: '#660000',
|
|
110
108
|
addedDimmed: '#3e515b',
|
|
111
109
|
removedDimmed: '#3e2c2c',
|
|
112
110
|
},
|
|
113
111
|
}
|
|
114
112
|
|
|
115
|
-
export type ThemeNames =
|
|
116
|
-
| 'dark'
|
|
117
|
-
| 'light'
|
|
118
|
-
| 'light-daltonized'
|
|
119
|
-
| 'dark-daltonized'
|
|
113
|
+
export type ThemeNames = 'dark' | 'light' | 'light-daltonized' | 'dark-daltonized'
|
|
120
114
|
|
|
121
115
|
export function getTheme(overrideTheme?: ThemeNames): Theme {
|
|
122
116
|
const config = getGlobalConfig()
|