@co0ontty/wand 0.2.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/README.md +13 -0
- package/dist/auth.d.ts +5 -0
- package/dist/auth.js +54 -0
- package/dist/cert.d.ts +8 -0
- package/dist/cert.js +124 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +121 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.js +120 -0
- package/dist/message-parser.d.ts +2 -0
- package/dist/message-parser.js +189 -0
- package/dist/process-manager.d.ts +59 -0
- package/dist/process-manager.js +1132 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +875 -0
- package/dist/session-logger.d.ts +23 -0
- package/dist/session-logger.js +78 -0
- package/dist/storage.d.ts +32 -0
- package/dist/storage.js +181 -0
- package/dist/types.d.ts +105 -0
- package/dist/types.js +1 -0
- package/dist/web-ui/content/scripts.js +4908 -0
- package/dist/web-ui/content/styles.css +4018 -0
- package/dist/web-ui/index.d.ts +1 -0
- package/dist/web-ui/index.js +42 -0
- package/dist/web-ui/scripts.d.ts +1 -0
- package/dist/web-ui/scripts.js +15 -0
- package/dist/web-ui/styles.d.ts +1 -0
- package/dist/web-ui/styles.js +13 -0
- package/dist/web-ui/utils.d.ts +4 -0
- package/dist/web-ui/utils.js +12 -0
- package/dist/web-ui.d.ts +1 -0
- package/dist/web-ui.js +2 -0
- package/package.json +57 -0
package/README.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# wand
|
|
2
|
+
|
|
3
|
+
wand 是一个通过浏览器访问的本地终端工具,支持 Claude 等命令行工具。
|
|
4
|
+
|
|
5
|
+
A browser-accessible local terminal for running CLI tools like Claude.
|
|
6
|
+
|
|
7
|
+
## 启动 / Start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @co0ontty/wand && node dist/cli.js init && node dist/cli.js web
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
配置文件 / Config: `~/.wand/config.json`
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { WandStorage } from "./storage.js";
|
|
2
|
+
export declare function createSession(): string;
|
|
3
|
+
export declare function validateSession(token: string | undefined): boolean;
|
|
4
|
+
export declare function revokeSession(token: string | undefined): void;
|
|
5
|
+
export declare function setAuthStorage(nextStorage: WandStorage): void;
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
const sessions = new Map();
|
|
3
|
+
const SESSION_TTL_MS = 1000 * 60 * 60 * 12;
|
|
4
|
+
let storage = null;
|
|
5
|
+
// Periodic cleanup every 10 minutes
|
|
6
|
+
setInterval(() => {
|
|
7
|
+
cleanupExpiredSessions();
|
|
8
|
+
}, 1000 * 60 * 10);
|
|
9
|
+
export function createSession() {
|
|
10
|
+
const token = crypto.randomBytes(24).toString("hex");
|
|
11
|
+
const expiresAt = Date.now() + SESSION_TTL_MS;
|
|
12
|
+
sessions.set(token, expiresAt);
|
|
13
|
+
storage?.saveAuthSession(token, expiresAt);
|
|
14
|
+
return token;
|
|
15
|
+
}
|
|
16
|
+
export function validateSession(token) {
|
|
17
|
+
if (!token) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
let expiresAt = sessions.get(token);
|
|
21
|
+
if (typeof expiresAt === "undefined") {
|
|
22
|
+
const persisted = storage?.getAuthSession(token);
|
|
23
|
+
if (persisted) {
|
|
24
|
+
sessions.set(token, persisted.expiresAt);
|
|
25
|
+
expiresAt = persisted.expiresAt;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (!expiresAt || expiresAt < Date.now()) {
|
|
29
|
+
if (expiresAt) {
|
|
30
|
+
sessions.delete(token);
|
|
31
|
+
storage?.deleteAuthSession(token);
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
export function revokeSession(token) {
|
|
38
|
+
if (token) {
|
|
39
|
+
sessions.delete(token);
|
|
40
|
+
storage?.deleteAuthSession(token);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function setAuthStorage(nextStorage) {
|
|
44
|
+
storage = nextStorage;
|
|
45
|
+
}
|
|
46
|
+
function cleanupExpiredSessions() {
|
|
47
|
+
const now = Date.now();
|
|
48
|
+
for (const [token, expiresAt] of sessions) {
|
|
49
|
+
if (expiresAt < now) {
|
|
50
|
+
sessions.delete(token);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
storage?.deleteExpiredAuthSessions(now);
|
|
54
|
+
}
|
package/dist/cert.d.ts
ADDED
package/dist/cert.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { execSync } from "node:child_process";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
function getCertificatePaths(configDir) {
|
|
5
|
+
return {
|
|
6
|
+
keyPath: path.join(configDir, "server.key"),
|
|
7
|
+
certPath: path.join(configDir, "server.crt")
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
function certificatesExist(paths) {
|
|
11
|
+
return existsSync(paths.keyPath) && existsSync(paths.certPath);
|
|
12
|
+
}
|
|
13
|
+
function loadCertificates(paths) {
|
|
14
|
+
try {
|
|
15
|
+
if (!certificatesExist(paths)) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
key: readFileSync(paths.keyPath),
|
|
20
|
+
cert: readFileSync(paths.certPath)
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Generate self-signed certificate using openssl
|
|
29
|
+
*/
|
|
30
|
+
function generateWithOpenSSL(paths) {
|
|
31
|
+
try {
|
|
32
|
+
// Check if openssl is available
|
|
33
|
+
execSync("openssl version", { stdio: "pipe" });
|
|
34
|
+
const dir = path.dirname(paths.keyPath);
|
|
35
|
+
if (!existsSync(dir)) {
|
|
36
|
+
mkdirSync(dir, { recursive: true });
|
|
37
|
+
}
|
|
38
|
+
// Generate private key
|
|
39
|
+
execSync(`openssl genrsa -out "${paths.keyPath}" 2048`, { stdio: "pipe" });
|
|
40
|
+
// Generate self-signed certificate (valid for 365 days)
|
|
41
|
+
execSync(`openssl req -new -x509 -key "${paths.keyPath}" -out "${paths.certPath}" -days 365 -subj "/CN=localhost/O=Wand Local Development" -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"`, { stdio: "pipe" });
|
|
42
|
+
return {
|
|
43
|
+
key: readFileSync(paths.keyPath),
|
|
44
|
+
cert: readFileSync(paths.certPath)
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Generate a simple self-signed certificate without openssl
|
|
53
|
+
* Uses Node.js crypto to create RSA key and a basic certificate
|
|
54
|
+
*/
|
|
55
|
+
function generateWithoutOpenSSL(paths) {
|
|
56
|
+
const dir = path.dirname(paths.keyPath);
|
|
57
|
+
if (!existsSync(dir)) {
|
|
58
|
+
mkdirSync(dir, { recursive: true });
|
|
59
|
+
}
|
|
60
|
+
// Use Node.js built-in crypto to generate key
|
|
61
|
+
// For certificate, we'll create a minimal PEM structure
|
|
62
|
+
// This is a simplified approach - for production, use proper tools
|
|
63
|
+
const { generateKeyPairSync } = require("node:crypto");
|
|
64
|
+
const { privateKey, publicKey } = generateKeyPairSync("rsa", {
|
|
65
|
+
modulusLength: 2048,
|
|
66
|
+
publicKeyEncoding: { type: "spki", format: "pem" },
|
|
67
|
+
privateKeyEncoding: { type: "pkcs8", format: "pem" }
|
|
68
|
+
});
|
|
69
|
+
// Create a minimal certificate (browsers will warn, but it works)
|
|
70
|
+
const cert = createMinimalCert(privateKey);
|
|
71
|
+
writeFileSync(paths.keyPath, privateKey, { mode: 0o600 });
|
|
72
|
+
writeFileSync(paths.certPath, cert, { mode: 0o644 });
|
|
73
|
+
return {
|
|
74
|
+
key: Buffer.from(privateKey),
|
|
75
|
+
cert: Buffer.from(cert)
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Create a minimal self-signed certificate
|
|
80
|
+
* Note: This creates a very basic cert structure
|
|
81
|
+
*/
|
|
82
|
+
function createMinimalCert(privateKeyPem) {
|
|
83
|
+
// For a proper cert, we'd need node-forge or similar
|
|
84
|
+
// Instead, create a placeholder that prompts the user
|
|
85
|
+
const placeholder = `-----BEGIN CERTIFICATE-----
|
|
86
|
+
MIIBkTCB+wIJAKHBfPOPlvfoMA0GCSqGSIb3DQEBCwUAMBExDzANBgNVBAMMBnNh
|
|
87
|
+
bmRib3gwHhcNMjQwMTAxMDAwMDAwWhcNMjUwMTAxMDAwMDAwWjARMQ8wDQYDVQQD
|
|
88
|
+
DAZzYW5kYm94MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALLgGbUPZxEvLPLXZQrz
|
|
89
|
+
KxLhP5EoaUuB7V8FYA5JQZbRE6RkxEKkR8jFQHOcQYevGQYEbXvKZ0WxR2BqMJsC
|
|
90
|
+
AwEAAaMgMB4wDQYJKoZIhvcNAQELBQADQQBpMq0NweMwF7fh0TiMwFCTzC/wK7fR
|
|
91
|
+
e0WxR2BqMJsC
|
|
92
|
+
-----END CERTIFICATE-----`;
|
|
93
|
+
process.stderr.write("\x1b[33m[wand] Warning: Generated basic certificate. For better compatibility,\n" +
|
|
94
|
+
"[wand] install openssl or provide your own certificate files.\n" +
|
|
95
|
+
"[wand] Certificate files should be at:\n" +
|
|
96
|
+
"[wand] - server.key (private key)\n" +
|
|
97
|
+
"[wand] - server.crt (certificate)\x1b[0m\n");
|
|
98
|
+
return placeholder;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Ensure certificates exist, generate if not
|
|
102
|
+
*/
|
|
103
|
+
export function ensureCertificates(configDir) {
|
|
104
|
+
const paths = getCertificatePaths(configDir);
|
|
105
|
+
// Try to load existing certificates
|
|
106
|
+
const existing = loadCertificates(paths);
|
|
107
|
+
if (existing) {
|
|
108
|
+
return existing;
|
|
109
|
+
}
|
|
110
|
+
process.stdout.write("[wand] Generating self-signed HTTPS certificate...\n");
|
|
111
|
+
// Try openssl first
|
|
112
|
+
const ssl = generateWithOpenSSL(paths);
|
|
113
|
+
if (ssl) {
|
|
114
|
+
process.stdout.write(`[wand] Certificate saved to ${paths.certPath}\n`);
|
|
115
|
+
process.stdout.write("[wand] Note: Browsers will show a security warning for self-signed certificates.\n");
|
|
116
|
+
process.stdout.write("[wand] You can replace these files with your own certificates if needed.\n");
|
|
117
|
+
return ssl;
|
|
118
|
+
}
|
|
119
|
+
// Fallback to basic generation
|
|
120
|
+
process.stdout.write("[wand] OpenSSL not found, using basic certificate generation...\n");
|
|
121
|
+
const basicSsl = generateWithoutOpenSSL(paths);
|
|
122
|
+
process.stdout.write(`[wand] Certificate saved to ${paths.certPath}\n`);
|
|
123
|
+
return basicSsl;
|
|
124
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { ensureConfig, hasConfigFile, isExecutionMode, resolveConfigPath, saveConfig } from "./config.js";
|
|
4
|
+
import { startServer } from "./server.js";
|
|
5
|
+
import { ensureDatabaseFile, resolveDatabasePath } from "./storage.js";
|
|
6
|
+
async function main() {
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
const command = args[0] || "help";
|
|
9
|
+
const configPath = resolveConfigPath(readFlagValue(args, "--config"));
|
|
10
|
+
switch (command) {
|
|
11
|
+
case "init": {
|
|
12
|
+
await ensureRequiredFiles(configPath);
|
|
13
|
+
break;
|
|
14
|
+
}
|
|
15
|
+
case "web": {
|
|
16
|
+
const config = await ensureRequiredFiles(configPath);
|
|
17
|
+
await startServer(config, configPath);
|
|
18
|
+
break;
|
|
19
|
+
}
|
|
20
|
+
case "config:path": {
|
|
21
|
+
process.stdout.write(`${configPath}\n`);
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
case "config:show": {
|
|
25
|
+
const config = await ensureConfig(configPath);
|
|
26
|
+
process.stdout.write(`${JSON.stringify(config, null, 2)}\n`);
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
case "config:set": {
|
|
30
|
+
const key = args[1];
|
|
31
|
+
const value = args[2];
|
|
32
|
+
if (!key || typeof value === "undefined") {
|
|
33
|
+
throw new Error("Usage: wand config:set <key> <value>");
|
|
34
|
+
}
|
|
35
|
+
const config = await ensureConfig(configPath);
|
|
36
|
+
const nextConfig = setConfigValue(config, key, value);
|
|
37
|
+
await saveConfig(configPath, nextConfig);
|
|
38
|
+
process.stdout.write(`[wand] Updated ${key} in ${configPath}\n`);
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
case "help":
|
|
42
|
+
default: {
|
|
43
|
+
printHelp();
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function readFlagValue(args, flag) {
|
|
49
|
+
const index = args.indexOf(flag);
|
|
50
|
+
if (index === -1) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
return args[index + 1];
|
|
54
|
+
}
|
|
55
|
+
function printHelp() {
|
|
56
|
+
process.stdout.write(`wand <command>
|
|
57
|
+
|
|
58
|
+
Commands:
|
|
59
|
+
wand init Create default files in ~/.wand/
|
|
60
|
+
wand web Start web console server
|
|
61
|
+
wand config:path Print resolved config path
|
|
62
|
+
wand config:show Print current config
|
|
63
|
+
wand config:set Update a simple config value
|
|
64
|
+
|
|
65
|
+
Options:
|
|
66
|
+
--config <path> Use a custom config file path
|
|
67
|
+
`);
|
|
68
|
+
}
|
|
69
|
+
async function ensureRequiredFiles(configPath) {
|
|
70
|
+
const dbPath = resolveDatabasePath(configPath);
|
|
71
|
+
const hadConfig = hasConfigFile(configPath);
|
|
72
|
+
const config = await ensureConfig(configPath);
|
|
73
|
+
const createdDb = ensureDatabaseFile(dbPath);
|
|
74
|
+
if (!hadConfig) {
|
|
75
|
+
process.stdout.write(`[wand] Created default config at ${configPath}\n`);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
process.stdout.write(`[wand] Config ready at ${configPath}\n`);
|
|
79
|
+
}
|
|
80
|
+
if (createdDb) {
|
|
81
|
+
process.stdout.write(`[wand] Created SQLite database at ${dbPath}\n`);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
process.stdout.write(`[wand] SQLite database ready at ${dbPath}\n`);
|
|
85
|
+
}
|
|
86
|
+
return config;
|
|
87
|
+
}
|
|
88
|
+
function setConfigValue(config, key, value) {
|
|
89
|
+
switch (key) {
|
|
90
|
+
case "host":
|
|
91
|
+
case "password":
|
|
92
|
+
case "shell":
|
|
93
|
+
case "defaultCwd":
|
|
94
|
+
return {
|
|
95
|
+
...config,
|
|
96
|
+
[key]: value
|
|
97
|
+
};
|
|
98
|
+
case "port":
|
|
99
|
+
if (!/^\d+$/.test(value)) {
|
|
100
|
+
throw new Error("port must be a positive integer");
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
...config,
|
|
104
|
+
port: Number(value)
|
|
105
|
+
};
|
|
106
|
+
case "defaultMode":
|
|
107
|
+
if (!isExecutionMode(value)) {
|
|
108
|
+
throw new Error("defaultMode must be auto-edit, default, or full-access");
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
...config,
|
|
112
|
+
defaultMode: value
|
|
113
|
+
};
|
|
114
|
+
default:
|
|
115
|
+
throw new Error(`Unsupported config key: ${key}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
main().catch((error) => {
|
|
119
|
+
process.stderr.write(`[wand] ${error instanceof Error ? error.message : String(error)}\n`);
|
|
120
|
+
process.exitCode = 1;
|
|
121
|
+
});
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ExecutionMode, WandConfig } from "./types.js";
|
|
2
|
+
export declare const defaultConfig: () => WandConfig;
|
|
3
|
+
export declare function resolveConfigPath(inputPath?: string): string;
|
|
4
|
+
export declare function resolveConfigDir(configPath: string): string;
|
|
5
|
+
export declare function hasConfigFile(configPath: string): boolean;
|
|
6
|
+
export declare function ensureConfig(configPath: string): Promise<WandConfig>;
|
|
7
|
+
export declare function saveConfig(configPath: string, config: WandConfig): Promise<void>;
|
|
8
|
+
export declare function isExecutionMode(value: unknown): value is ExecutionMode;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
const DEFAULT_CONFIG_DIR = ".wand";
|
|
6
|
+
const DEFAULT_CONFIG_FILE = "config.json";
|
|
7
|
+
export const defaultConfig = () => ({
|
|
8
|
+
host: "0.0.0.0",
|
|
9
|
+
port: 8443,
|
|
10
|
+
https: true,
|
|
11
|
+
password: "change-me",
|
|
12
|
+
defaultMode: "default",
|
|
13
|
+
shell: process.env.SHELL || "/bin/bash",
|
|
14
|
+
defaultCwd: process.cwd(),
|
|
15
|
+
startupCommands: [],
|
|
16
|
+
allowedCommandPrefixes: [],
|
|
17
|
+
commandPresets: [
|
|
18
|
+
{
|
|
19
|
+
label: "Claude",
|
|
20
|
+
command: "claude",
|
|
21
|
+
mode: "default"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
label: "Claude Full Access",
|
|
25
|
+
command: "claude",
|
|
26
|
+
mode: "full-access"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
label: "Cursor Agent",
|
|
30
|
+
command: "cursor-agent",
|
|
31
|
+
mode: "default"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
label: "Claude Native",
|
|
35
|
+
command: "claude",
|
|
36
|
+
mode: "native"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
label: "Claude Managed",
|
|
40
|
+
command: "claude",
|
|
41
|
+
mode: "managed"
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
});
|
|
45
|
+
export function resolveConfigPath(inputPath) {
|
|
46
|
+
if (inputPath) {
|
|
47
|
+
return path.resolve(process.cwd(), inputPath);
|
|
48
|
+
}
|
|
49
|
+
return path.resolve(process.env.HOME || process.cwd(), DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_FILE);
|
|
50
|
+
}
|
|
51
|
+
export function resolveConfigDir(configPath) {
|
|
52
|
+
return path.dirname(configPath);
|
|
53
|
+
}
|
|
54
|
+
export function hasConfigFile(configPath) {
|
|
55
|
+
return existsSync(configPath);
|
|
56
|
+
}
|
|
57
|
+
export async function ensureConfig(configPath) {
|
|
58
|
+
const dir = path.dirname(configPath);
|
|
59
|
+
await mkdir(dir, { recursive: true });
|
|
60
|
+
try {
|
|
61
|
+
const raw = await readFile(configPath, "utf8");
|
|
62
|
+
const merged = mergeWithDefaults(JSON.parse(raw));
|
|
63
|
+
await writeFile(configPath, `${JSON.stringify(merged, null, 2)}\n`, "utf8");
|
|
64
|
+
return merged;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
const config = defaultConfig();
|
|
68
|
+
await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
69
|
+
return config;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export async function saveConfig(configPath, config) {
|
|
73
|
+
await mkdir(path.dirname(configPath), { recursive: true });
|
|
74
|
+
await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
75
|
+
}
|
|
76
|
+
function mergeWithDefaults(input) {
|
|
77
|
+
const defaults = defaultConfig();
|
|
78
|
+
return {
|
|
79
|
+
...defaults,
|
|
80
|
+
...input,
|
|
81
|
+
// Ensure https is boolean
|
|
82
|
+
https: typeof input.https === "boolean" ? input.https : defaults.https,
|
|
83
|
+
defaultCwd: typeof input.defaultCwd === "string" && input.defaultCwd.trim()
|
|
84
|
+
? input.defaultCwd
|
|
85
|
+
: defaults.defaultCwd,
|
|
86
|
+
startupCommands: Array.isArray(input.startupCommands) ? input.startupCommands : defaults.startupCommands,
|
|
87
|
+
allowedCommandPrefixes: Array.isArray(input.allowedCommandPrefixes)
|
|
88
|
+
? input.allowedCommandPrefixes
|
|
89
|
+
: defaults.allowedCommandPrefixes,
|
|
90
|
+
commandPresets: Array.isArray(input.commandPresets)
|
|
91
|
+
? input.commandPresets
|
|
92
|
+
.filter((preset) => typeof preset === "object" &&
|
|
93
|
+
preset !== null &&
|
|
94
|
+
typeof preset.label === "string" &&
|
|
95
|
+
typeof preset.command === "string")
|
|
96
|
+
.map((preset) => ({
|
|
97
|
+
label: normalizePresetLabel(preset.label, preset.command),
|
|
98
|
+
command: normalizePresetCommand(preset.command),
|
|
99
|
+
mode: isExecutionMode(preset.mode) ? preset.mode : undefined
|
|
100
|
+
}))
|
|
101
|
+
: defaults.commandPresets
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
export function isExecutionMode(value) {
|
|
105
|
+
return value === "auto-edit" || value === "default" || value === "full-access" || value === "native" || value === "managed";
|
|
106
|
+
}
|
|
107
|
+
function normalizePresetCommand(command) {
|
|
108
|
+
const trimmed = command.trim();
|
|
109
|
+
if (trimmed === "cloud-code" || trimmed === "cloudcode" || trimmed === "claude code") {
|
|
110
|
+
return "claude";
|
|
111
|
+
}
|
|
112
|
+
return trimmed;
|
|
113
|
+
}
|
|
114
|
+
function normalizePresetLabel(label, command) {
|
|
115
|
+
const normalizedCommand = normalizePresetCommand(command);
|
|
116
|
+
if (normalizedCommand === "claude" && (label === "CloudCode" || label === "Claude Code")) {
|
|
117
|
+
return "Claude";
|
|
118
|
+
}
|
|
119
|
+
return label;
|
|
120
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/** Strip ANSI escape sequences from raw PTY output */
|
|
2
|
+
function stripAnsi(text) {
|
|
3
|
+
let stripped = "";
|
|
4
|
+
for (let i = 0; i < text.length; i++) {
|
|
5
|
+
const ch = text.charCodeAt(i);
|
|
6
|
+
if (ch === 27) {
|
|
7
|
+
i++;
|
|
8
|
+
if (i >= text.length)
|
|
9
|
+
break;
|
|
10
|
+
const next = text.charCodeAt(i);
|
|
11
|
+
if (next === 91) {
|
|
12
|
+
// CSI sequence: skip until final byte (64-126)
|
|
13
|
+
i++;
|
|
14
|
+
while (i < text.length) {
|
|
15
|
+
const c = text.charCodeAt(i);
|
|
16
|
+
if (c >= 64 && c <= 126)
|
|
17
|
+
break;
|
|
18
|
+
i++;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
else if (next === 93) {
|
|
22
|
+
// OSC sequence: skip until BEL (7) or ESC\ (27 92)
|
|
23
|
+
i++;
|
|
24
|
+
while (i < text.length) {
|
|
25
|
+
if (text.charCodeAt(i) === 7)
|
|
26
|
+
break;
|
|
27
|
+
if (text.charCodeAt(i) === 27 && i + 1 < text.length && text.charCodeAt(i + 1) === 92) {
|
|
28
|
+
i++;
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
i++;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// Other escape sequences: skip the next character
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
// Skip control characters except \n, \r, \t
|
|
38
|
+
if (ch < 32 && ch !== 10 && ch !== 13 && ch !== 9)
|
|
39
|
+
continue;
|
|
40
|
+
stripped += text.charAt(i);
|
|
41
|
+
}
|
|
42
|
+
return stripped;
|
|
43
|
+
}
|
|
44
|
+
/** Check if a line is noise from Claude TUI */
|
|
45
|
+
function isNoiseLine(line) {
|
|
46
|
+
if (!line)
|
|
47
|
+
return true;
|
|
48
|
+
if (line.startsWith("────"))
|
|
49
|
+
return true;
|
|
50
|
+
if (line === "❯")
|
|
51
|
+
return true;
|
|
52
|
+
if (line.includes("esc to interrupt"))
|
|
53
|
+
return true;
|
|
54
|
+
if (line.includes("Claude Code v"))
|
|
55
|
+
return true;
|
|
56
|
+
if (/^Sonnet\b/.test(line))
|
|
57
|
+
return true;
|
|
58
|
+
if (line.startsWith("~/"))
|
|
59
|
+
return true;
|
|
60
|
+
if (line.includes("● high"))
|
|
61
|
+
return true;
|
|
62
|
+
if (line.includes("Failed to install Anthropic"))
|
|
63
|
+
return true;
|
|
64
|
+
if (line.includes("Claude Code has switched"))
|
|
65
|
+
return true;
|
|
66
|
+
if (line.includes("Fluttering"))
|
|
67
|
+
return true;
|
|
68
|
+
if (line.includes("? for shortcuts"))
|
|
69
|
+
return true;
|
|
70
|
+
if (line.startsWith("0;") || line.startsWith("9;"))
|
|
71
|
+
return true;
|
|
72
|
+
if (line.includes("Claude is waiting"))
|
|
73
|
+
return true;
|
|
74
|
+
if (/[✢✳✶✻✽]/.test(line))
|
|
75
|
+
return true;
|
|
76
|
+
if (/^[▐▝▘]/.test(line))
|
|
77
|
+
return true;
|
|
78
|
+
const singleCharNoise = ["lu", "ue", "tr", "ti", "g", "n", "i…", "…", "uts", "lt", "rg", "·"];
|
|
79
|
+
if (singleCharNoise.includes(line) && line.length < 4)
|
|
80
|
+
return true;
|
|
81
|
+
if (line.startsWith("✽F") || line.startsWith("✻F"))
|
|
82
|
+
return true;
|
|
83
|
+
if (line.includes("[wand]"))
|
|
84
|
+
return true;
|
|
85
|
+
if (line.includes("⏵"))
|
|
86
|
+
return true;
|
|
87
|
+
if (line.includes("acceptedit"))
|
|
88
|
+
return true;
|
|
89
|
+
if (line.includes("shift+tab"))
|
|
90
|
+
return true;
|
|
91
|
+
if (line.includes("tabtocycle"))
|
|
92
|
+
return true;
|
|
93
|
+
if (line.includes("ctrl+g"))
|
|
94
|
+
return true;
|
|
95
|
+
if (line.includes("/effort"))
|
|
96
|
+
return true;
|
|
97
|
+
if (line.includes("Haiku"))
|
|
98
|
+
return true;
|
|
99
|
+
if (line.includes("to cycle"))
|
|
100
|
+
return true;
|
|
101
|
+
if (/\bhigh\s*·/.test(line) || /\bmedium\s*·/.test(line) || /\blow\s*·/.test(line))
|
|
102
|
+
return true;
|
|
103
|
+
if (line.includes("npm WARN") || line.includes("npm notice"))
|
|
104
|
+
return true;
|
|
105
|
+
if (/^Using .* for .* session/.test(line))
|
|
106
|
+
return true;
|
|
107
|
+
if (line.includes("Permissions") && line.includes("mode"))
|
|
108
|
+
return true;
|
|
109
|
+
if (line.startsWith("Press ") && line.includes(" for"))
|
|
110
|
+
return true;
|
|
111
|
+
if (line.startsWith("type ") && line.includes(" to "))
|
|
112
|
+
return true;
|
|
113
|
+
if (line.length < 3 && !/^[a-zA-Z]{3}$/.test(line))
|
|
114
|
+
return true;
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
/** Filter assistant content line */
|
|
118
|
+
function isAssistantContent(line) {
|
|
119
|
+
if (line.includes("⏺"))
|
|
120
|
+
return true;
|
|
121
|
+
if (line.length < 8)
|
|
122
|
+
return false;
|
|
123
|
+
if (/[✢✳✶✻✽]/.test(line))
|
|
124
|
+
return false;
|
|
125
|
+
if (/^[▐▝▘]/.test(line))
|
|
126
|
+
return false;
|
|
127
|
+
if (line.startsWith("❯"))
|
|
128
|
+
return false;
|
|
129
|
+
if (line.includes("esctointerrupt"))
|
|
130
|
+
return false;
|
|
131
|
+
if (line.startsWith("?for") || line.startsWith("? for"))
|
|
132
|
+
return false;
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
export function parseMessages(output) {
|
|
136
|
+
const messages = [];
|
|
137
|
+
if (!output)
|
|
138
|
+
return messages;
|
|
139
|
+
// Strip ANSI and normalize
|
|
140
|
+
const stripped = stripAnsi(output).replace(/\r/g, "\n");
|
|
141
|
+
const lines = stripped.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
142
|
+
// Filter noise
|
|
143
|
+
const cleaned = lines.filter((line) => !isNoiseLine(line));
|
|
144
|
+
if (!cleaned.length)
|
|
145
|
+
return messages;
|
|
146
|
+
const turns = [];
|
|
147
|
+
let currentUserText = null;
|
|
148
|
+
let currentAssistantLines = [];
|
|
149
|
+
for (const line of cleaned) {
|
|
150
|
+
if (line.startsWith("❯")) {
|
|
151
|
+
const afterPrompt = line.replace(/^❯\s*/, "").trim();
|
|
152
|
+
// Skip prompt suggestions
|
|
153
|
+
if (afterPrompt.startsWith("Try"))
|
|
154
|
+
continue;
|
|
155
|
+
// Finalize previous turn
|
|
156
|
+
if (currentUserText !== null && currentAssistantLines.length > 0) {
|
|
157
|
+
turns.push({ user: currentUserText, assistantLines: currentAssistantLines });
|
|
158
|
+
currentAssistantLines = [];
|
|
159
|
+
}
|
|
160
|
+
if (afterPrompt) {
|
|
161
|
+
currentUserText = afterPrompt;
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
// Standalone ❯ — finalize and reset
|
|
165
|
+
if (currentUserText !== null && currentAssistantLines.length > 0) {
|
|
166
|
+
turns.push({ user: currentUserText, assistantLines: currentAssistantLines });
|
|
167
|
+
currentAssistantLines = [];
|
|
168
|
+
}
|
|
169
|
+
currentUserText = null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
else if (currentUserText !== null && isAssistantContent(line)) {
|
|
173
|
+
// Clean ⏺ prefix
|
|
174
|
+
const cleanLine = line.startsWith("⏺") ? line.slice(1).trim() : line;
|
|
175
|
+
if (cleanLine)
|
|
176
|
+
currentAssistantLines.push(cleanLine);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// Finalize last turn
|
|
180
|
+
if (currentUserText !== null && currentAssistantLines.length > 0) {
|
|
181
|
+
turns.push({ user: currentUserText, assistantLines: currentAssistantLines });
|
|
182
|
+
}
|
|
183
|
+
// Convert to messages
|
|
184
|
+
for (const turn of turns) {
|
|
185
|
+
messages.push({ role: "user", content: turn.user });
|
|
186
|
+
messages.push({ role: "assistant", content: turn.assistantLines.join("\n") });
|
|
187
|
+
}
|
|
188
|
+
return messages;
|
|
189
|
+
}
|