@miruzero/zero-agent 0.3.6 → 0.3.8

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.
Files changed (68) hide show
  1. package/README.md +31 -22
  2. package/package.json +6 -3
  3. package/src/agent.js +193 -167
  4. package/src/args.js +1 -3
  5. package/src/autostart.js +44 -68
  6. package/src/capabilities.js +95 -138
  7. package/src/chatgpt/auth/index.js +114 -0
  8. package/src/chatgpt/config/paths.js +25 -0
  9. package/src/chatgpt/index.js +5 -0
  10. package/src/chatgpt/session/direct-transport.js +68 -0
  11. package/src/chatgpt/session/exchange.js +17 -0
  12. package/src/chatgpt/session/index.js +102 -0
  13. package/src/chatgpt/session/local-storage.js +106 -0
  14. package/src/chatgpt/transport/direct/http-client.js +191 -0
  15. package/src/chatgpt-auth.js +17 -111
  16. package/src/chatgpt-runtime.js +8 -0
  17. package/src/chatgpt-session-exchange.js +8 -0
  18. package/src/chatgpt-session.js +26 -0
  19. package/src/cli.js +108 -71
  20. package/src/command/exec.js +34 -0
  21. package/src/command/filesystem/append-file.js +7 -12
  22. package/src/command/filesystem/copy-directory.js +8 -12
  23. package/src/command/filesystem/copy-file.js +8 -13
  24. package/src/command/filesystem/create-directory.js +7 -11
  25. package/src/command/filesystem/create-file.js +8 -13
  26. package/src/command/filesystem/delete-directory.js +7 -11
  27. package/src/command/filesystem/delete-file.js +8 -12
  28. package/src/command/filesystem/exists.js +6 -10
  29. package/src/command/filesystem/get-file-info.js +7 -12
  30. package/src/command/filesystem/glob-files.js +8 -13
  31. package/src/command/filesystem/grep-files.js +9 -15
  32. package/src/command/filesystem/hash-file.js +6 -11
  33. package/src/command/filesystem/index.js +33 -34
  34. package/src/command/filesystem/list-directory.js +6 -10
  35. package/src/command/filesystem/move-directory.js +9 -14
  36. package/src/command/filesystem/move-file.js +8 -13
  37. package/src/command/filesystem/path-policy.js +33 -16
  38. package/src/command/filesystem/read-file.js +7 -11
  39. package/src/command/filesystem/read-files.js +5 -8
  40. package/src/command/filesystem/rename-file.js +5 -8
  41. package/src/command/filesystem/replace-file.js +7 -12
  42. package/src/command/filesystem/text.js +5 -7
  43. package/src/command/filesystem/touch-file.js +8 -13
  44. package/src/command/filesystem/truncate-file.js +6 -10
  45. package/src/command/filesystem/walk.js +36 -39
  46. package/src/command/filesystem/write-file.js +8 -13
  47. package/src/command/index.js +19 -24
  48. package/src/command/path/index.js +46 -53
  49. package/src/config.js +21 -33
  50. package/src/device-paths.js +53 -0
  51. package/src/enrollment-client.js +19 -0
  52. package/src/http.js +27 -2
  53. package/src/identity-state.js +27 -0
  54. package/src/index.js +2 -4
  55. package/src/instance.js +85 -0
  56. package/src/local-server.js +174 -0
  57. package/src/local-tools.js +205 -205
  58. package/src/shared-zero/catalog.js +33 -0
  59. package/src/shared-zero/contract.js +18 -0
  60. package/src/shared-zero/descriptor.js +25 -0
  61. package/src/shared-zero/errors.js +12 -0
  62. package/src/shared-zero/index.js +9 -0
  63. package/src/shared-zero/runtime.js +14 -0
  64. package/src/shared-zero/target.js +19 -0
  65. package/src/shared-zero/tool-id.js +26 -0
  66. package/src/storage-roots.js +21 -0
  67. package/src/zero-runtime.js +1 -11
  68. package/src/zero.js +78 -0
package/src/autostart.js CHANGED
@@ -1,85 +1,74 @@
1
1
  'use strict';
2
2
 
3
- const childProcess = require('node:child_process');
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
- const DEFAULT_INTERVAL = 5000;
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
- const defaultRoots = (env = process.env) => {
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
- const paths = (env = process.env) => {
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
- const packageBin = (env = process.env) => {
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
- const nodeBin = (env = process.env) => path.resolve(env.ZERO_AGENT_NODE || process.execPath);
55
+ exports.nodeBin = (env = process.env) => zero.path.resolve(env.ZERO_AGENT_NODE || process.execPath);
67
56
 
68
- const launcherContent = ({ interval = DEFAULT_INTERVAL, roots, logFile, binFile, nodeFile }, env = process.env) => {
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
- const prepare = (options = {}, env = process.env) => {
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
- const isPrepared = (env = process.env) => {
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);
@@ -1,63 +1,58 @@
1
- 'use strict';
2
-
3
- const crypto = require('node:crypto');
4
- const fs = require('node:fs');
5
- const path = require('node:path');
6
- const localTools = require('./local-tools');
7
- const command = require('./command');
8
- const zero = require('./zero-runtime');
9
-
10
- const defaultMcpRoot = () => path.resolve(__dirname, '../..');
11
- const mcpRoot = (env = process.env) => path.resolve(env.ZERO_MCP_ROOT || defaultMcpRoot());
12
- const providersDir = (env = process.env) => path.join(mcpRoot(env), 'providers');
13
-
14
- const readManifest = (file) => {
15
- try {
16
- return JSON.parse(fs.readFileSync(file, 'utf8'));
17
- } catch (error) {
18
- return { error: error.message };
19
- }
20
- };
21
-
22
- const manifestProviders = (env = process.env) => {
23
- const dir = providersDir(env);
24
- if (!fs.existsSync(dir)) return [];
25
- return fs.readdirSync(dir, { withFileTypes: true })
26
- .filter((item) => item.isDirectory())
27
- .map((item) => {
28
- const root = path.join(dir, item.name);
29
- const manifestFile = path.join(root, 'provider.json');
30
- const manifest = fs.existsSync(manifestFile) ? readManifest(manifestFile) : {};
31
- return {
32
- name: item.name,
33
- type: manifest.type || 'unknown',
34
- description: manifest.description || '',
35
- providerRoot: root,
36
- manifestFile,
37
- manifestError: manifest.error || null,
38
- module: manifest.module || null,
39
- command: manifest.command || null,
40
- args: manifest.args || []
41
- };
42
- });
43
- };
44
-
45
- const safeInternalTools = (provider) => {
46
- if (!provider.module) return [];
47
- const modulePath = provider.module.replaceAll('{providerRoot}', provider.providerRoot.replace(/\\/g, '/'));
48
- const resolved = path.resolve(modulePath);
49
- try {
50
- const runtime = require(resolved);
51
- if (typeof runtime.listTools !== 'function') return [];
52
- return runtime.listTools({}).map((tool) => ({
53
- name: tool.name,
54
- description: tool.description || '',
55
- inputSchema: tool.inputSchema || null
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',
@@ -80,81 +75,43 @@ const builtInLocalProvider = () => {
80
75
  };
81
76
  };
82
77
 
83
- const builtInChatGptAuthProvider = () => {
84
- const tools = [
85
- {
86
- name: 'status',
87
- description: 'Return redacted ChatGPT auth fixture status from the local zero-agent host.',
88
- canonicalName: 'zero.chatgpt.auth.status',
89
- category: 'auth',
90
- function: 'status',
91
- aliases: ['chatgpt.auth.status'],
92
- inputSchema: { type: 'object', properties: {}, additionalProperties: false }
93
- },
94
- {
95
- name: 'import',
96
- description: 'Write ChatGPT auth fixture input to the local zero-agent host. Returns redacted metadata only.',
97
- canonicalName: 'zero.chatgpt.auth.import',
98
- category: 'auth',
99
- function: 'import',
100
- aliases: ['chatgpt.auth.import'],
101
- inputSchema: {
102
- type: 'object',
103
- properties: { input: { type: 'string' } },
104
- required: ['input'],
105
- additionalProperties: false
106
- }
107
- }
108
- ];
109
- return {
110
- name: 'chatgpt',
111
- type: 'internal',
112
- description: 'Built-in ChatGPT auth setup support exposed by zero-agent.',
113
- status: 'present',
114
- tools,
115
- tool_count: tools.length
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
116
113
  };
117
- };
118
-
119
- const buildCapabilities = (env = process.env) => {
120
- const providers = [builtInLocalProvider(), builtInChatGptAuthProvider(), ...manifestProviders(env).map((provider) => {
121
- const tools = provider.type === 'internal' ? safeInternalTools(provider) : [];
122
- return {
123
- name: provider.name,
124
- type: provider.type,
125
- description: provider.description,
126
- status: provider.manifestError ? 'invalid' : 'present',
127
- tools,
128
- tool_count: tools.length,
129
- mcp_stdio_live_scan: provider.type === 'mcp-stdio' ? false : undefined
130
- };
131
- })];
132
- const agentVersion = require('../package.json').version;
133
- const categories = Array.from(new Set(command.listTools().map((tool) => `${tool.provider}.${tool.category}`))).sort();
134
- const adapters = ['native'];
135
- const payload = {
136
- agentVersion,
137
- agent_version: agentVersion,
138
- contractVersion: zero.contract.version,
139
- platform: process.platform,
140
- architecture: process.arch,
141
- categories,
142
- adapters,
143
- mcp_root: mcpRoot(env),
144
- providers
145
- };
146
- const fingerprint = {
147
- contractVersion: payload.contractVersion,
148
- platform: payload.platform,
149
- architecture: payload.architecture,
150
- categories: payload.categories,
151
- adapters: payload.adapters,
152
- providers: payload.providers
153
- };
154
- payload.capability_hash = crypto.createHash('sha256').update(JSON.stringify(fingerprint)).digest('hex');
155
- return payload;
156
- };
157
-
158
- const capabilities = { mcpRoot, providersDir, manifestProviders, buildCapabilities };
159
- Object.assign(exports, capabilities);
160
-
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,5 @@
1
+ 'use strict';
2
+
3
+ exports.paths = require('./config/paths');
4
+ exports.auth = require('./auth');
5
+ exports.session = require('./session');
@@ -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
+ };