@miruzero/zero-agent 0.3.5 → 0.3.7
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 +31 -22
- package/package.json +6 -3
- package/src/agent.js +193 -158
- package/src/args.js +1 -3
- package/src/autostart.js +44 -68
- package/src/capabilities.js +98 -104
- package/src/chatgpt/auth/index.js +114 -0
- package/src/chatgpt/config/paths.js +25 -0
- package/src/chatgpt/index.js +5 -0
- package/src/chatgpt/session/direct-transport.js +68 -0
- package/src/chatgpt/session/exchange.js +17 -0
- package/src/chatgpt/session/index.js +102 -0
- package/src/chatgpt/session/local-storage.js +106 -0
- package/src/chatgpt/transport/direct/http-client.js +191 -0
- package/src/chatgpt-auth.js +27 -0
- package/src/chatgpt-runtime.js +8 -0
- package/src/chatgpt-session-exchange.js +8 -0
- package/src/chatgpt-session.js +26 -0
- package/src/cli.js +108 -71
- package/src/command/exec.js +34 -0
- package/src/command/filesystem/append-file.js +7 -12
- package/src/command/filesystem/copy-directory.js +8 -12
- package/src/command/filesystem/copy-file.js +8 -13
- package/src/command/filesystem/create-directory.js +7 -11
- package/src/command/filesystem/create-file.js +8 -13
- package/src/command/filesystem/delete-directory.js +7 -11
- package/src/command/filesystem/delete-file.js +8 -12
- package/src/command/filesystem/exists.js +6 -10
- package/src/command/filesystem/get-file-info.js +7 -12
- package/src/command/filesystem/glob-files.js +8 -13
- package/src/command/filesystem/grep-files.js +9 -15
- package/src/command/filesystem/hash-file.js +6 -11
- package/src/command/filesystem/index.js +33 -34
- package/src/command/filesystem/list-directory.js +6 -10
- package/src/command/filesystem/move-directory.js +9 -14
- package/src/command/filesystem/move-file.js +8 -13
- package/src/command/filesystem/path-policy.js +33 -16
- package/src/command/filesystem/read-file.js +7 -11
- package/src/command/filesystem/read-files.js +5 -8
- package/src/command/filesystem/rename-file.js +5 -8
- package/src/command/filesystem/replace-file.js +7 -12
- package/src/command/filesystem/text.js +5 -7
- package/src/command/filesystem/touch-file.js +8 -13
- package/src/command/filesystem/truncate-file.js +6 -10
- package/src/command/filesystem/walk.js +36 -39
- package/src/command/filesystem/write-file.js +8 -13
- package/src/command/index.js +19 -24
- package/src/command/path/index.js +46 -53
- package/src/config.js +21 -33
- package/src/device-paths.js +53 -0
- package/src/enrollment-client.js +19 -0
- package/src/http.js +27 -2
- package/src/identity-state.js +27 -0
- package/src/index.js +2 -4
- package/src/instance.js +85 -0
- package/src/local-server.js +174 -0
- package/src/local-tools.js +205 -205
- package/src/shared-zero/catalog.js +33 -0
- package/src/shared-zero/contract.js +18 -0
- package/src/shared-zero/descriptor.js +25 -0
- package/src/shared-zero/errors.js +12 -0
- package/src/shared-zero/index.js +9 -0
- package/src/shared-zero/runtime.js +14 -0
- package/src/shared-zero/target.js +19 -0
- package/src/shared-zero/tool-id.js +26 -0
- package/src/storage-roots.js +21 -0
- package/src/zero-runtime.js +1 -11
- package/src/zero.js +78 -0
package/src/autostart.js
CHANGED
|
@@ -1,85 +1,74 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
const fs = require('node:fs');
|
|
5
|
-
const os = require('node:os');
|
|
6
|
-
const path = require('node:path');
|
|
7
|
-
const config = require('./config');
|
|
8
|
-
const pkg = require('../package.json');
|
|
3
|
+
const zero = require('./zero');
|
|
9
4
|
|
|
10
|
-
|
|
5
|
+
|
|
6
|
+
exports.DEFAULT_INTERVAL = 5000;
|
|
11
7
|
|
|
12
8
|
const q = (value) => '"' + String(value).replace(/"/g, '""') + '"';
|
|
13
9
|
const winPath = (value) => String(value).replace(/\//g, '\\');
|
|
14
10
|
|
|
15
|
-
|
|
16
|
-
if (env.ZERO_AGENT_ROOTS) return env.ZERO_AGENT_ROOTS;
|
|
17
|
-
const home = env.USERPROFILE || os.homedir();
|
|
18
|
-
return [
|
|
19
|
-
path.join(home, 'Desktop', 'zero'),
|
|
20
|
-
path.join(home, 'Downloads')
|
|
21
|
-
].join(path.delimiter);
|
|
22
|
-
};
|
|
11
|
+
exports.defaultRoots = () => null;
|
|
23
12
|
|
|
24
13
|
const startupDir = (env = process.env) => {
|
|
25
|
-
const appData = env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
|
|
26
|
-
return path.join(appData, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup');
|
|
14
|
+
const appData = env.APPDATA || zero.path.join(zero.os.homedir(), 'AppData', 'Roaming');
|
|
15
|
+
return zero.path.join(appData, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup');
|
|
27
16
|
};
|
|
28
|
-
|
|
29
|
-
const home = config.agentHome(env);
|
|
17
|
+
exports.paths = (env = process.env) => {
|
|
18
|
+
const home = zero.config.agentHome(env);
|
|
30
19
|
return {
|
|
31
20
|
home,
|
|
32
|
-
launcherFile: path.join(home, 'start-zero-agent.cmd'),
|
|
33
|
-
startupFile: path.join(startupDir(env), 'MiruZeroAgent.vbs'),
|
|
34
|
-
logFile: path.join(config.logDir(env), 'zero-agent.log')
|
|
21
|
+
launcherFile: zero.path.join(home, 'start-zero-agent.cmd'),
|
|
22
|
+
startupFile: zero.path.join(startupDir(env), 'MiruZeroAgent.vbs'),
|
|
23
|
+
logFile: zero.path.join(zero.config.logDir(env), 'zero-agent.log')
|
|
35
24
|
};
|
|
36
25
|
};
|
|
37
26
|
|
|
38
|
-
const packageSpec = () => pkg.name + '@' + pkg.version;
|
|
39
|
-
const localPackageBin = () => path.join(__dirname, '..', 'bin', 'zero-agent.js');
|
|
27
|
+
const packageSpec = () => zero.pkg.name + '@' + zero.pkg.version;
|
|
28
|
+
const localPackageBin = () => zero.path.join(__dirname, '..', 'bin', 'zero-agent.js');
|
|
40
29
|
|
|
41
30
|
const globalRoot = (env = process.env) => {
|
|
42
|
-
if (env.ZERO_AGENT_GLOBAL_ROOT) return path.resolve(env.ZERO_AGENT_GLOBAL_ROOT);
|
|
31
|
+
if (env.ZERO_AGENT_GLOBAL_ROOT) return zero.path.resolve(env.ZERO_AGENT_GLOBAL_ROOT);
|
|
43
32
|
const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
44
|
-
const result = childProcess.spawnSync(npmCmd, ['root', '-g'], {
|
|
33
|
+
const result = zero.childProcess.spawnSync(npmCmd, ['root', '-g'], {
|
|
45
34
|
env,
|
|
46
35
|
encoding: 'utf8',
|
|
47
36
|
windowsHide: true
|
|
48
37
|
});
|
|
49
38
|
if (result.status !== 0) return null;
|
|
50
39
|
const value = String(result.stdout || '').trim();
|
|
51
|
-
return value ? path.resolve(value) : null;
|
|
40
|
+
return value ? zero.path.resolve(value) : null;
|
|
52
41
|
};
|
|
53
42
|
|
|
54
|
-
|
|
55
|
-
if (env.ZERO_AGENT_BIN) return path.resolve(env.ZERO_AGENT_BIN);
|
|
43
|
+
exports.packageBin = (env = process.env) => {
|
|
44
|
+
if (env.ZERO_AGENT_BIN) return zero.path.resolve(env.ZERO_AGENT_BIN);
|
|
56
45
|
if (env.ZERO_AGENT_SKIP_GLOBAL_INSTALL !== '1') {
|
|
57
46
|
const root = globalRoot(env);
|
|
58
47
|
const candidate = root
|
|
59
|
-
? path.join(root, '@miruzero', 'zero-agent', 'bin', 'zero-agent.js')
|
|
48
|
+
? zero.path.join(root, '@miruzero', 'zero-agent', 'bin', 'zero-agent.js')
|
|
60
49
|
: null;
|
|
61
|
-
if (candidate && fs.existsSync(candidate)) return candidate;
|
|
50
|
+
if (candidate && zero.fs.existsSync(candidate)) return candidate;
|
|
62
51
|
}
|
|
63
52
|
return localPackageBin();
|
|
64
53
|
};
|
|
65
54
|
|
|
66
|
-
|
|
55
|
+
exports.nodeBin = (env = process.env) => zero.path.resolve(env.ZERO_AGENT_NODE || process.execPath);
|
|
67
56
|
|
|
68
|
-
|
|
69
|
-
const home = config.agentHome(env);
|
|
57
|
+
exports.launcherContent = ({ interval = exports.DEFAULT_INTERVAL, roots, logFile, binFile, nodeFile }, env = process.env) => {
|
|
58
|
+
const home = zero.config.agentHome(env);
|
|
70
59
|
const spec = packageSpec();
|
|
71
|
-
const bin = binFile || packageBin(env);
|
|
72
|
-
const node = nodeFile || nodeBin(env);
|
|
60
|
+
const bin = binFile || exports.packageBin(env);
|
|
61
|
+
const node = nodeFile || exports.nodeBin(env);
|
|
73
62
|
return [
|
|
74
63
|
'@echo off',
|
|
75
64
|
'setlocal',
|
|
76
65
|
'set "ZERO_TRACE=1"',
|
|
77
66
|
'set "ZERO_AGENT_HOME=' + winPath(home) + '"',
|
|
78
|
-
'set "ZERO_AGENT_ROOTS=' + winPath(roots) + '"',
|
|
67
|
+
...(roots ? ['set "ZERO_AGENT_ROOTS=' + winPath(roots) + '"'] : []),
|
|
79
68
|
...(env.ZERO_MCP_ROOT ? ['set "ZERO_MCP_ROOT=' + winPath(env.ZERO_MCP_ROOT) + '"'] : []),
|
|
80
|
-
'set "ZERO_AGENT_INTERVAL=' + Number(interval || DEFAULT_INTERVAL) + '"',
|
|
81
|
-
'rem interval=' + Number(interval || DEFAULT_INTERVAL) + 'ms',
|
|
82
|
-
'if not exist ' + q(winPath(path.dirname(logFile))) + ' mkdir ' + q(winPath(path.dirname(logFile))),
|
|
69
|
+
'set "ZERO_AGENT_INTERVAL=' + Number(interval || exports.DEFAULT_INTERVAL) + '"',
|
|
70
|
+
'rem interval=' + Number(interval || exports.DEFAULT_INTERVAL) + 'ms',
|
|
71
|
+
'if not exist ' + q(winPath(zero.path.dirname(logFile))) + ' mkdir ' + q(winPath(zero.path.dirname(logFile))),
|
|
83
72
|
'echo [%date% %time%] starting ' + spec + ' >> ' + q(winPath(logFile)),
|
|
84
73
|
q(winPath(node)) + ' ' + q(winPath(bin)) + ' start --interval %ZERO_AGENT_INTERVAL% >> ' + q(winPath(logFile)) + ' 2>&1',
|
|
85
74
|
''
|
|
@@ -92,41 +81,28 @@ const vbsContent = (launcherFile) => [
|
|
|
92
81
|
''
|
|
93
82
|
].join('\r\n');
|
|
94
83
|
|
|
95
|
-
|
|
96
|
-
config.ensureDirs(env);
|
|
97
|
-
const p = paths(env);
|
|
98
|
-
const interval = Number(options.interval || env.ZERO_AGENT_INTERVAL || DEFAULT_INTERVAL);
|
|
99
|
-
const roots = options.roots || defaultRoots(env);
|
|
100
|
-
const binFile = packageBin(env);
|
|
101
|
-
const nodeFile = nodeBin(env);
|
|
102
|
-
fs.mkdirSync(path.dirname(p.launcherFile), { recursive: true });
|
|
103
|
-
fs.mkdirSync(path.dirname(p.startupFile), { recursive: true });
|
|
104
|
-
fs.mkdirSync(path.dirname(p.logFile), { recursive: true });
|
|
105
|
-
fs.writeFileSync(p.launcherFile, launcherContent({
|
|
84
|
+
exports.prepare = (options = {}, env = process.env) => {
|
|
85
|
+
zero.config.ensureDirs(env);
|
|
86
|
+
const p = exports.paths(env);
|
|
87
|
+
const interval = Number(options.interval || env.ZERO_AGENT_INTERVAL || exports.DEFAULT_INTERVAL);
|
|
88
|
+
const roots = options.roots || exports.defaultRoots(env);
|
|
89
|
+
const binFile = exports.packageBin(env);
|
|
90
|
+
const nodeFile = exports.nodeBin(env);
|
|
91
|
+
zero.fs.mkdirSync(zero.path.dirname(p.launcherFile), { recursive: true });
|
|
92
|
+
zero.fs.mkdirSync(zero.path.dirname(p.startupFile), { recursive: true });
|
|
93
|
+
zero.fs.mkdirSync(zero.path.dirname(p.logFile), { recursive: true });
|
|
94
|
+
zero.fs.writeFileSync(p.launcherFile, exports.launcherContent({
|
|
106
95
|
interval,
|
|
107
96
|
roots,
|
|
108
97
|
logFile: p.logFile,
|
|
109
98
|
binFile,
|
|
110
99
|
nodeFile
|
|
111
100
|
}, env), 'utf8');
|
|
112
|
-
fs.writeFileSync(p.startupFile, vbsContent(p.launcherFile), 'utf8');
|
|
101
|
+
zero.fs.writeFileSync(p.startupFile, vbsContent(p.launcherFile), 'utf8');
|
|
113
102
|
return { ok: true, interval, roots, binFile, nodeFile, ...p };
|
|
114
103
|
};
|
|
115
104
|
|
|
116
|
-
|
|
117
|
-
const p = paths(env);
|
|
118
|
-
return fs.existsSync(p.launcherFile) && fs.existsSync(p.startupFile);
|
|
105
|
+
exports.isPrepared = (env = process.env) => {
|
|
106
|
+
const p = exports.paths(env);
|
|
107
|
+
return zero.fs.existsSync(p.launcherFile) && zero.fs.existsSync(p.startupFile);
|
|
119
108
|
};
|
|
120
|
-
|
|
121
|
-
const autostart = {
|
|
122
|
-
DEFAULT_INTERVAL,
|
|
123
|
-
defaultRoots,
|
|
124
|
-
paths,
|
|
125
|
-
prepare,
|
|
126
|
-
isPrepared,
|
|
127
|
-
packageBin,
|
|
128
|
-
nodeBin,
|
|
129
|
-
launcherContent
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
Object.assign(exports, autostart);
|
package/src/capabilities.js
CHANGED
|
@@ -1,63 +1,58 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}));
|
|
57
|
-
} catch (error) {
|
|
58
|
-
return [{ name: '__scan_error__', description: error.message, inputSchema: null }];
|
|
59
|
-
}
|
|
60
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const zero = require('./zero');
|
|
4
|
+
|
|
5
|
+
const defaultMcpRoot = () => zero.path.resolve(__dirname, '..');
|
|
6
|
+
exports.mcpRoot = (env = process.env) => zero.path.resolve(env.ZERO_MCP_ROOT || defaultMcpRoot());
|
|
7
|
+
exports.providersDir = (env = process.env) => zero.path.join(exports.mcpRoot(env), 'providers');
|
|
8
|
+
|
|
9
|
+
const readManifest = (file) => {
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(zero.fs.readFileSync(file, 'utf8'));
|
|
12
|
+
} catch (error) {
|
|
13
|
+
return { error: error.message };
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
exports.manifestProviders = (env = process.env) => {
|
|
18
|
+
const dir = exports.providersDir(env);
|
|
19
|
+
if (!zero.fs.existsSync(dir)) return [];
|
|
20
|
+
return zero.fs.readdirSync(dir, { withFileTypes: true })
|
|
21
|
+
.filter((item) => item.isDirectory())
|
|
22
|
+
.map((item) => {
|
|
23
|
+
const root = zero.path.join(dir, item.name);
|
|
24
|
+
const manifestFile = zero.path.join(root, 'provider.json');
|
|
25
|
+
const manifest = zero.fs.existsSync(manifestFile) ? readManifest(manifestFile) : {};
|
|
26
|
+
return {
|
|
27
|
+
name: item.name,
|
|
28
|
+
type: manifest.type || 'unknown',
|
|
29
|
+
description: manifest.description || '',
|
|
30
|
+
providerRoot: root,
|
|
31
|
+
manifestFile,
|
|
32
|
+
manifestError: manifest.error || null,
|
|
33
|
+
module: manifest.module || null,
|
|
34
|
+
command: manifest.command || null,
|
|
35
|
+
args: manifest.args || []
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const safeInternalTools = (provider) => {
|
|
41
|
+
if (!provider.module) return [];
|
|
42
|
+
const modulePath = provider.module.replaceAll('{providerRoot}', provider.providerRoot.replace(/\\/g, '/'));
|
|
43
|
+
const resolved = zero.path.resolve(modulePath);
|
|
44
|
+
try {
|
|
45
|
+
const runtime = require(resolved);
|
|
46
|
+
if (typeof runtime.listTools !== 'function') return [];
|
|
47
|
+
return runtime.listTools({}).map((tool) => ({
|
|
48
|
+
name: tool.name,
|
|
49
|
+
description: tool.description || '',
|
|
50
|
+
inputSchema: tool.inputSchema || null
|
|
51
|
+
}));
|
|
52
|
+
} catch (error) {
|
|
53
|
+
return [{ name: '__scan_error__', description: error.message, inputSchema: null }];
|
|
54
|
+
}
|
|
55
|
+
};
|
|
61
56
|
const publicCommandTool = (tool) => ({
|
|
62
57
|
name: tool.name,
|
|
63
58
|
description: tool.description || '',
|
|
@@ -69,7 +64,7 @@ const publicCommandTool = (tool) => ({
|
|
|
69
64
|
});
|
|
70
65
|
|
|
71
66
|
const builtInLocalProvider = () => {
|
|
72
|
-
const tools = command.listTools().map(publicCommandTool);
|
|
67
|
+
const tools = zero.command.listTools().map(publicCommandTool);
|
|
73
68
|
return {
|
|
74
69
|
name: 'zero-agent-command',
|
|
75
70
|
type: 'internal',
|
|
@@ -78,46 +73,45 @@ const builtInLocalProvider = () => {
|
|
|
78
73
|
tools,
|
|
79
74
|
tool_count: tools.length
|
|
80
75
|
};
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const providers = [builtInLocalProvider(), ...manifestProviders(env).map((provider) => {
|
|
85
|
-
const tools = provider.type === 'internal' ? safeInternalTools(provider) : [];
|
|
86
|
-
return {
|
|
87
|
-
name: provider.name,
|
|
88
|
-
type: provider.type,
|
|
89
|
-
description: provider.description,
|
|
90
|
-
status: provider.manifestError ? 'invalid' : 'present',
|
|
91
|
-
tools,
|
|
92
|
-
tool_count: tools.length,
|
|
93
|
-
mcp_stdio_live_scan: provider.type === 'mcp-stdio' ? false : undefined
|
|
94
|
-
};
|
|
95
|
-
})];
|
|
96
|
-
const agentVersion =
|
|
97
|
-
const categories = Array.from(new Set(command.listTools().map((tool) => `${tool.provider}.${tool.category}`))).sort();
|
|
98
|
-
const adapters = ['native'];
|
|
99
|
-
const payload = {
|
|
100
|
-
agentVersion,
|
|
101
|
-
agent_version: agentVersion,
|
|
102
|
-
contractVersion: zero.contract.version,
|
|
103
|
-
platform: process.platform,
|
|
104
|
-
architecture: process.arch,
|
|
105
|
-
categories,
|
|
106
|
-
adapters,
|
|
107
|
-
mcp_root: mcpRoot(env),
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
Object.assign(exports, capabilities);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
exports.buildCapabilities = (env = process.env) => {
|
|
79
|
+
const providers = [builtInLocalProvider(), ...exports.manifestProviders(env).map((provider) => {
|
|
80
|
+
const tools = provider.type === 'internal' ? safeInternalTools(provider) : [];
|
|
81
|
+
return {
|
|
82
|
+
name: provider.name,
|
|
83
|
+
type: provider.type,
|
|
84
|
+
description: provider.description,
|
|
85
|
+
status: provider.manifestError ? 'invalid' : 'present',
|
|
86
|
+
tools,
|
|
87
|
+
tool_count: tools.length,
|
|
88
|
+
mcp_stdio_live_scan: provider.type === 'mcp-stdio' ? false : undefined
|
|
89
|
+
};
|
|
90
|
+
})];
|
|
91
|
+
const agentVersion = zero.pkg.version;
|
|
92
|
+
const categories = Array.from(new Set(zero.command.listTools().map((tool) => `${tool.provider}.${tool.category}`))).sort();
|
|
93
|
+
const adapters = ['native'];
|
|
94
|
+
const payload = {
|
|
95
|
+
agentVersion,
|
|
96
|
+
agent_version: agentVersion,
|
|
97
|
+
contractVersion: zero.contract.version,
|
|
98
|
+
platform: process.platform,
|
|
99
|
+
architecture: process.arch,
|
|
100
|
+
categories,
|
|
101
|
+
adapters,
|
|
102
|
+
mcp_root: exports.mcpRoot(env),
|
|
103
|
+
filesystem: zero.devicePaths.snapshot(env),
|
|
104
|
+
providers
|
|
105
|
+
};
|
|
106
|
+
const fingerprint = {
|
|
107
|
+
contractVersion: payload.contractVersion,
|
|
108
|
+
platform: payload.platform,
|
|
109
|
+
architecture: payload.architecture,
|
|
110
|
+
categories: payload.categories,
|
|
111
|
+
adapters: payload.adapters,
|
|
112
|
+
providers: payload.providers
|
|
113
|
+
};
|
|
114
|
+
payload.capability_hash = zero.crypto.createHash('sha256').update(JSON.stringify(fingerprint)).digest('hex');
|
|
115
|
+
return payload;
|
|
116
|
+
};
|
|
117
|
+
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const paths = require('../config/paths');
|
|
6
|
+
|
|
7
|
+
const readJson = (file) => { try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; } };
|
|
8
|
+
const writeJson = (file, value) => { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 }); };
|
|
9
|
+
const tokenExpiry = (token) => {
|
|
10
|
+
try {
|
|
11
|
+
const payload = JSON.parse(Buffer.from(String(token).split('.')[1], 'base64url').toString('utf8'));
|
|
12
|
+
return Number.isFinite(payload.exp) ? new Date(payload.exp * 1000).toISOString() : null;
|
|
13
|
+
} catch { return null; }
|
|
14
|
+
};
|
|
15
|
+
const isExpired = (expires) => { const at = Date.parse(expires || ''); return Number.isFinite(at) && at <= Date.now(); };
|
|
16
|
+
const rawFromEnv = (env) => {
|
|
17
|
+
const token = typeof env.ZERO_CHATGPT_ACCESS_TOKEN === 'string' ? env.ZERO_CHATGPT_ACCESS_TOKEN.trim() : '';
|
|
18
|
+
if (!token) return null;
|
|
19
|
+
return { tokenName: 'env', accessToken: token, expires: tokenExpiry(token), source: 'env' };
|
|
20
|
+
};
|
|
21
|
+
exports.file = (env = process.env) => paths.authFile(env);
|
|
22
|
+
|
|
23
|
+
const rawFromFile = (env) => {
|
|
24
|
+
const file = paths.authFile(env);
|
|
25
|
+
const data = readJson(file);
|
|
26
|
+
if (!data || typeof data.accessToken !== 'string' || !data.accessToken) return null;
|
|
27
|
+
return { tokenName: data.tokenName || 'ACCESS', accessToken: data.accessToken, expires: data.expires || tokenExpiry(data.accessToken), updatedAt: data.updatedAt || null, source: 'file', file };
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
exports.status = ({ env = process.env } = {}) => {
|
|
31
|
+
const raw = rawFromEnv(env) || rawFromFile(env);
|
|
32
|
+
const file = paths.authFile(env);
|
|
33
|
+
if (!raw) return { present: false, status: fs.existsSync(file) ? 'invalid' : 'missing', redacted: true, source: 'none', file };
|
|
34
|
+
return { present: true, status: isExpired(raw.expires) ? 'expired' : 'valid', redacted: true, source: raw.source, tokenName: raw.tokenName, expires: raw.expires || null, updatedAt: raw.updatedAt || null, file: raw.file || null };
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
exports.importSession = ({ body, env = process.env, source = 'session' } = {}) => {
|
|
38
|
+
const accessToken = body?.accessToken || body?.access_token;
|
|
39
|
+
if (!accessToken || typeof accessToken !== 'string') {
|
|
40
|
+
throw Object.assign(new Error('ChatGPT session did not provide an access token'), { code: 'AUTH_REQUIRED' });
|
|
41
|
+
}
|
|
42
|
+
const expires = body?.expires || body?.expires_at || tokenExpiry(accessToken);
|
|
43
|
+
const value = {
|
|
44
|
+
tokenName: 'ACCESS',
|
|
45
|
+
accessToken,
|
|
46
|
+
expires: expires || null,
|
|
47
|
+
updatedAt: new Date().toISOString(),
|
|
48
|
+
source
|
|
49
|
+
};
|
|
50
|
+
writeJson(paths.authFile(env), value);
|
|
51
|
+
return {
|
|
52
|
+
present: true,
|
|
53
|
+
status: isExpired(value.expires) ? 'expired' : 'valid',
|
|
54
|
+
redacted: true,
|
|
55
|
+
source,
|
|
56
|
+
tokenName: value.tokenName,
|
|
57
|
+
expires: value.expires,
|
|
58
|
+
updatedAt: value.updatedAt,
|
|
59
|
+
file: paths.authFile(env)
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
exports.refresh = async ({ env = process.env, acquireSession, fresh = false } = {}) => {
|
|
64
|
+
const flow = typeof acquireSession === 'function' ? acquireSession : require('../session').acquire;
|
|
65
|
+
const result = await flow({ env, fresh });
|
|
66
|
+
const body = result?.body ?? result;
|
|
67
|
+
const accessToken = body?.accessToken || body?.access_token;
|
|
68
|
+
if (!accessToken || typeof accessToken !== 'string') throw Object.assign(new Error('ChatGPT session did not provide an access token'), { code: 'AUTH_REQUIRED' });
|
|
69
|
+
const value = {
|
|
70
|
+
tokenName: 'ACCESS',
|
|
71
|
+
accessToken,
|
|
72
|
+
expires: body?.expires || body?.expires_at || tokenExpiry(accessToken),
|
|
73
|
+
updatedAt: new Date().toISOString()
|
|
74
|
+
};
|
|
75
|
+
writeJson(paths.authFile(env), value);
|
|
76
|
+
return { ...value, source: 'session' };
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
exports.login = async ({ env = process.env, acquireSession } = {}) => {
|
|
80
|
+
await exports.refresh({ env, fresh: true, ...(typeof acquireSession === 'function' ? { acquireSession } : {}) });
|
|
81
|
+
return exports.status({ env });
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
exports.logout = ({ env = process.env } = {}) => {
|
|
85
|
+
const authFile = paths.authFile(env);
|
|
86
|
+
const sessionFile = paths.sessionFile(env);
|
|
87
|
+
const remove = (file) => {
|
|
88
|
+
const existed = fs.existsSync(file);
|
|
89
|
+
if (existed) fs.rmSync(file, { force: true });
|
|
90
|
+
return existed;
|
|
91
|
+
};
|
|
92
|
+
const authRemoved = remove(authFile);
|
|
93
|
+
const sessionRemoved = remove(sessionFile);
|
|
94
|
+
const envOverridePresent = typeof env.ZERO_CHATGPT_ACCESS_TOKEN === 'string' && Boolean(env.ZERO_CHATGPT_ACCESS_TOKEN.trim());
|
|
95
|
+
return {
|
|
96
|
+
status: envOverridePresent ? 'env-active' : 'logged-out',
|
|
97
|
+
redacted: true,
|
|
98
|
+
authRemoved,
|
|
99
|
+
sessionRemoved,
|
|
100
|
+
envOverridePresent
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
exports.ensure = async ({ env = process.env, acquireSession } = {}) => {
|
|
104
|
+
const envAuth = rawFromEnv(env);
|
|
105
|
+
if (envAuth && !isExpired(envAuth.expires)) return envAuth;
|
|
106
|
+
const fileAuth = rawFromFile(env);
|
|
107
|
+
if (fileAuth && !isExpired(fileAuth.expires)) return fileAuth;
|
|
108
|
+
if (fileAuth && isExpired(fileAuth.expires)) {
|
|
109
|
+
try { fs.rmSync(paths.authFile(env), { force: true }); } catch {}
|
|
110
|
+
}
|
|
111
|
+
if (acquireSession === false) throw Object.assign(new Error('ChatGPT authentication required'), { code: 'AUTH_REQUIRED' });
|
|
112
|
+
try { return await exports.refresh({ env, ...(typeof acquireSession === 'function' ? { acquireSession } : {}) }); }
|
|
113
|
+
catch (error) { if (error?.code) throw error; throw Object.assign(new Error('ChatGPT authentication required'), { code: 'AUTH_REQUIRED', cause: error }); }
|
|
114
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
const crypto = require('node:crypto');
|
|
6
|
+
|
|
7
|
+
const agentHome = (env = process.env) =>
|
|
8
|
+
path.resolve(env.ZERO_AGENT_HOME || path.join(os.homedir(), '.zero-agent'));
|
|
9
|
+
|
|
10
|
+
exports.stateDir = (env = process.env) =>
|
|
11
|
+
path.resolve(env.ZERO_CHATGPT_STATE_DIR || path.join(agentHome(env), 'chatgpt'));
|
|
12
|
+
exports.authFile = (env = process.env) =>
|
|
13
|
+
path.resolve(env.ZERO_CHATGPT_AUTH_FILE || path.join(exports.stateDir(env), 'auth-context.json'));
|
|
14
|
+
exports.sessionFile = (env = process.env) =>
|
|
15
|
+
path.resolve(env.ZERO_CHATGPT_SESSION_FILE || path.join(exports.stateDir(env), 'session-context.json'));
|
|
16
|
+
exports.tempConversationFile = (env = process.env) =>
|
|
17
|
+
path.resolve(env.ZERO_CHATGPT_TEMP_STATE_FILE || path.join(exports.stateDir(env), 'temp-conversations.json'));
|
|
18
|
+
exports.rateLimitDir = (env = process.env) => env.ZERO_CHATGPT_RATE_LIMIT_FILE
|
|
19
|
+
? path.dirname(path.resolve(env.ZERO_CHATGPT_RATE_LIMIT_FILE))
|
|
20
|
+
: exports.stateDir(env);
|
|
21
|
+
exports.rateLimitFile = (env = process.env, token = 'anonymous') => {
|
|
22
|
+
if (env.ZERO_CHATGPT_RATE_LIMIT_FILE) return path.resolve(env.ZERO_CHATGPT_RATE_LIMIT_FILE);
|
|
23
|
+
const key = crypto.createHash('sha256').update(String(token || 'anonymous')).digest('hex').slice(0, 16);
|
|
24
|
+
return path.join(exports.rateLimitDir(env), `rate-limit-${key}.json`);
|
|
25
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const paths = require('../config/paths');
|
|
5
|
+
const direct = require('../transport/direct/http-client');
|
|
6
|
+
|
|
7
|
+
const readJson = (file) => { try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; } };
|
|
8
|
+
const mergeSetCookies = (cookieString, setCookie) => {
|
|
9
|
+
const jar = new Map();
|
|
10
|
+
const add = (entry) => {
|
|
11
|
+
const first = String(entry || '').split(';')[0];
|
|
12
|
+
const index = first.indexOf('=');
|
|
13
|
+
if (index > 0) jar.set(first.slice(0, index).trim(), first.slice(index + 1).trim());
|
|
14
|
+
};
|
|
15
|
+
for (const pair of String(cookieString || '').split(';')) add(pair);
|
|
16
|
+
for (const entry of Array.isArray(setCookie) ? setCookie : [setCookie]) add(entry);
|
|
17
|
+
return [...jar].map(([name, value]) => `${name}=${value}`).join('; ');
|
|
18
|
+
};
|
|
19
|
+
const bootstrapHeaders = (env) => {
|
|
20
|
+
const state = readJson(paths.sessionFile(env)) || {};
|
|
21
|
+
const saved = state.headers && typeof state.headers === 'object' && !Array.isArray(state.headers) ? state.headers : {};
|
|
22
|
+
const { Cookie: _cookie, cookie: _cookieLower, authorization: _authorization, ...safe } = saved;
|
|
23
|
+
return {
|
|
24
|
+
...safe,
|
|
25
|
+
origin: safe.origin || 'https://chatgpt.com',
|
|
26
|
+
referer: safe.referer || 'https://chatgpt.com/',
|
|
27
|
+
accept: safe.accept || 'application/json'
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
exports.createTransport = ({ requestJson = direct.requestJson } = {}) => async ({ sessionMarker, workspaceAccountId = null, env = process.env } = {}) => {
|
|
32
|
+
if (!sessionMarker || typeof sessionMarker !== 'string') {
|
|
33
|
+
throw Object.assign(new Error('Missing local ChatGPT session marker'), { code: 'CHATGPT_SESSION_MARKER_MISSING' });
|
|
34
|
+
}
|
|
35
|
+
const sessionFile = paths.sessionFile(env);
|
|
36
|
+
const sessionHeaders = bootstrapHeaders(env);
|
|
37
|
+
const first = await requestJson('/api/auth/session', sessionMarker, sessionHeaders, {
|
|
38
|
+
route: '/api/auth/session', env, sessionFile, persistSession: false
|
|
39
|
+
});
|
|
40
|
+
if (![200, 403].includes(Number(first?.status))) {
|
|
41
|
+
throw Object.assign(new Error('ChatGPT session bootstrap round one failed'), { code: 'CHATGPT_SESSION_BOOTSTRAP_FAILED', status: first?.status || 0 });
|
|
42
|
+
}
|
|
43
|
+
const cookie = mergeSetCookies('', first?.headers?.['set-cookie']);
|
|
44
|
+
if (!cookie) {
|
|
45
|
+
throw Object.assign(new Error('ChatGPT session bootstrap did not return a cookie'), { code: 'CHATGPT_SESSION_BOOTSTRAP_COOKIE_MISSING', status: first?.status || 0 });
|
|
46
|
+
}
|
|
47
|
+
sessionHeaders.Cookie = cookie;
|
|
48
|
+
const switched = await requestJson('/account-switch/switch', null, sessionHeaders, {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
body: workspaceAccountId ? { workspaceAccountId } : {},
|
|
51
|
+
route: '/account-switch/switch',
|
|
52
|
+
headers: { 'OAI-Account-Switch-Session-Token': sessionMarker },
|
|
53
|
+
env,
|
|
54
|
+
sessionFile,
|
|
55
|
+
persistSession: false
|
|
56
|
+
});
|
|
57
|
+
if (!switched || switched.status < 200 || switched.status >= 300) {
|
|
58
|
+
throw Object.assign(new Error('ChatGPT direct session exchange failed'), { code: 'CHATGPT_SESSION_EXCHANGE_FAILED', status: switched?.status || 0 });
|
|
59
|
+
}
|
|
60
|
+
const switchedCookie = mergeSetCookies(sessionHeaders.Cookie, switched?.headers?.['set-cookie']);
|
|
61
|
+
if (switchedCookie) sessionHeaders.Cookie = switchedCookie;
|
|
62
|
+
const second = await requestJson('/api/auth/session', null, sessionHeaders, {
|
|
63
|
+
route: '/api/auth/session', env, sessionFile, persistSession: false
|
|
64
|
+
});
|
|
65
|
+
return { status: Number(second?.status || 0), body: second?.json ?? null, sessionHeaders };
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
exports.transport = exports.createTransport();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
exports.SESSION_SOURCE = 'https://chatgpt.com/api/auth/session';
|
|
4
|
+
exports.normalizeResult = (result) => {
|
|
5
|
+
if (!result || typeof result !== 'object') throw Object.assign(new Error('ChatGPT session exchange returned an invalid result'), { code: 'CHATGPT_SESSION_EXCHANGE_INVALID_RESULT' });
|
|
6
|
+
return {
|
|
7
|
+
status: Number(result.status || 0),
|
|
8
|
+
body: result.body ?? result.data ?? null,
|
|
9
|
+
source: exports.SESSION_SOURCE,
|
|
10
|
+
sessionHeaders: result.sessionHeaders && typeof result.sessionHeaders === 'object' ? { ...result.sessionHeaders } : null
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
exports.exchangeSession = async ({ sessionMarker, workspaceAccountId = null, profile, env = process.env, transport } = {}) => {
|
|
14
|
+
if (!sessionMarker || typeof sessionMarker !== 'string') throw Object.assign(new Error('Missing local ChatGPT session marker'), { code: 'CHATGPT_SESSION_MARKER_MISSING' });
|
|
15
|
+
if (typeof transport !== 'function') throw Object.assign(new Error('ChatGPT session exchange transport is required'), { code: 'CHATGPT_SESSION_EXCHANGE_TRANSPORT_REQUIRED' });
|
|
16
|
+
return exports.normalizeResult(await transport({ sessionMarker, workspaceAccountId, profile: profile || null, env, sessionSource: exports.SESSION_SOURCE }));
|
|
17
|
+
};
|