@miruzero/zgpt 0.1.0 → 0.1.1
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/docs/architecture.md +29 -3
- package/package.json +1 -1
- package/src/auth/index.js +111 -84
- package/src/cli/conversations-tui.js +301 -0
- package/src/cli/formatter.js +39 -1
- package/src/cli.js +177 -113
- package/src/config/paths.js +3 -1
- package/src/connector/read.js +28 -4
- package/src/connector/write.js +7 -7
- package/src/conversation/context-config.js +68 -0
- package/src/conversation/index.js +11 -5
- package/src/conversation/read.js +10 -3
- package/src/conversation/recovery-state.js +51 -0
- package/src/conversation/recovery.js +85 -0
- package/src/conversation/temporary.js +3 -1
- package/src/conversation/write.js +118 -27
- package/src/file/index.js +151 -0
- package/src/index.js +5 -1
- package/src/memory/index.js +64 -0
- package/src/protocol/sse.js +2 -1
- package/src/runtime-context.js +27 -0
- package/src/session/direct-transport.js +68 -0
- package/src/session/exchange.js +9 -4
- package/src/session/index.js +100 -68
- package/src/session/local-storage.js +99 -68
- package/src/settings/index.js +18 -0
- package/src/share/index.js +25 -0
- package/src/transport/direct/http-client.js +189 -164
package/src/cli.js
CHANGED
|
@@ -1,117 +1,181 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
const fs = require('node:fs');
|
|
5
|
-
const path = require('node:path');
|
|
6
|
-
const formatter = require('./cli/formatter');
|
|
7
|
-
|
|
8
|
-
const usage = () => [
|
|
9
|
-
'Zero ChatGPT CLI',
|
|
10
|
-
'',
|
|
11
|
-
'Usage:',
|
|
12
|
-
' zgpt
|
|
13
|
-
' zgpt
|
|
14
|
-
' zgpt
|
|
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
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (!
|
|
56
|
-
stderr(`
|
|
57
|
-
return 2;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const formatter = require('./cli/formatter');
|
|
7
|
+
|
|
8
|
+
const usage = () => [
|
|
9
|
+
'Zero ChatGPT CLI',
|
|
10
|
+
'',
|
|
11
|
+
'Usage:',
|
|
12
|
+
' zgpt login',
|
|
13
|
+
' zgpt logout',
|
|
14
|
+
' zgpt auth-status',
|
|
15
|
+
' zgpt conversations list [--json]',
|
|
16
|
+
' zgpt retry <conversation-id>',
|
|
17
|
+
' zgpt get <conversation-id> [--limit N|A-B] [--debug|--json]',
|
|
18
|
+
' zgpt export <conversation-id> [--limit N|A-B] [--debug|--json] [--out <path>]',
|
|
19
|
+
' zgpt help'
|
|
20
|
+
].join('\n');
|
|
21
|
+
|
|
22
|
+
const parseOptions = (argv) => {
|
|
23
|
+
const result = { json: false, debug: false, limit: null, out: null };
|
|
24
|
+
for (let index = 2; index < argv.length; index += 1) {
|
|
25
|
+
const arg = argv[index];
|
|
26
|
+
if (arg === '--json' || arg === 'json') result.json = true;
|
|
27
|
+
else if (arg === '--debug' || arg === 'debug') result.debug = true;
|
|
28
|
+
else if (arg === '--limit' || arg === 'limit') {
|
|
29
|
+
const value = argv[++index];
|
|
30
|
+
result.limit = formatter.parseLimitSpec(value);
|
|
31
|
+
if (!result.limit) throw new Error(`Invalid limit: ${value || '(missing)'}`);
|
|
32
|
+
} else if (arg === '--out' || arg === 'out') {
|
|
33
|
+
result.out = argv[++index] || null;
|
|
34
|
+
if (!result.out) throw new Error('Export path is required after --out');
|
|
35
|
+
} else throw new Error(`Unknown option: ${arg}`);
|
|
36
|
+
}
|
|
37
|
+
if (result.json && result.debug) throw new Error('--json and --debug are mutually exclusive');
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const defaultExportPath = (conversationId, options, cwd) => {
|
|
42
|
+
const safeId = String(conversationId).replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
43
|
+
const suffix = options.json ? '.json' : options.debug ? '.debug.md' : '.md';
|
|
44
|
+
return path.join(cwd, 'exports', `conversation-${safeId}${suffix}`);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
exports.run = async (argv = [], options = {}) => {
|
|
48
|
+
const stdout = options.stdout || console.log;
|
|
49
|
+
const stderr = options.stderr || console.error;
|
|
50
|
+
const command = argv[0] || 'help';
|
|
51
|
+
if (command === 'help' || command === '--help' || command === '-h') {
|
|
52
|
+
stdout(usage());
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
if (!['login', 'logout', 'auth-status', 'conversations', 'get', 'export', 'stream-status', 'retry', 'resume'].includes(command)) {
|
|
56
|
+
stderr(`Unknown command: ${command}\n\n${usage()}`);
|
|
57
|
+
return 2;
|
|
58
|
+
}
|
|
59
|
+
if (command === 'login' || command === 'logout' || command === 'auth-status') {
|
|
60
|
+
if (argv.length !== 1) {
|
|
61
|
+
stderr(`Unknown option for ${command}\n\n${usage()}`);
|
|
62
|
+
return 2;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const runtime = options.runtime || require('./index');
|
|
66
|
+
if (command === 'login') {
|
|
67
|
+
let state;
|
|
68
|
+
if (typeof runtime.auth.login === 'function') state = await runtime.auth.login();
|
|
69
|
+
else {
|
|
70
|
+
await runtime.auth.ensure();
|
|
71
|
+
state = runtime.auth.status();
|
|
72
|
+
}
|
|
73
|
+
stdout(`status=${state?.status || '-'}\nredacted=${state?.redacted !== false}\nsource=${state?.source || '-'}\nexpires=${state?.expires || '-'}`);
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
if (command === 'auth-status') {
|
|
77
|
+
const state = runtime.auth.status();
|
|
78
|
+
stdout(`status=${state?.status || '-'}\nredacted=${state?.redacted !== false}\nsource=${state?.source || '-'}\nexpires=${state?.expires || '-'}`);
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
const state = await runtime.auth.logout();
|
|
82
|
+
stdout(`status=${state?.status || '-'}\nauth_removed=${Boolean(state?.authRemoved)}\nsession_removed=${Boolean(state?.sessionRemoved)}`);
|
|
83
|
+
return 0;
|
|
84
|
+
} catch (error) {
|
|
85
|
+
stderr(`${error?.code ? `${error.code}: ` : ''}${error?.message || String(error)}`);
|
|
86
|
+
return 1;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (command === 'conversations') {
|
|
90
|
+
if (argv[1] !== 'list') {
|
|
91
|
+
stderr(`Usage: zgpt conversations list [--json]\n\n${usage()}`);
|
|
92
|
+
return 2;
|
|
93
|
+
}
|
|
94
|
+
let json = false;
|
|
95
|
+
for (let index = 2; index < argv.length; index += 1) {
|
|
96
|
+
const arg = argv[index];
|
|
97
|
+
if (arg === '--json' || arg === 'json') json = true;
|
|
98
|
+
else {
|
|
99
|
+
stderr(`Unknown option for conversations list: ${arg}\n\n${usage()}`);
|
|
100
|
+
return 2;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const runtime = options.runtime || require('./index');
|
|
105
|
+
const items = await runtime.conversation.list();
|
|
106
|
+
if (json) {
|
|
107
|
+
stdout(formatter.json(items).trimEnd());
|
|
83
108
|
return 0;
|
|
84
109
|
}
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
110
|
+
const runTui = options.runConversationsTui || require('./cli/conversations-tui').run;
|
|
111
|
+
return await runTui({
|
|
112
|
+
items,
|
|
113
|
+
loadItems: () => runtime.conversation.list(),
|
|
114
|
+
openConversation: (id) => runtime.conversation.get(id),
|
|
115
|
+
formatConversation: (conversation) => formatter.markdown(conversation).trimEnd(),
|
|
116
|
+
input: options.stdin || process.stdin,
|
|
117
|
+
output: options.output || process.stdout
|
|
118
|
+
});
|
|
119
|
+
} catch (error) {
|
|
120
|
+
stderr(`${error?.code ? `${error.code}: ` : ''}${error?.message || String(error)}`);
|
|
121
|
+
return 1;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const conversationId = argv[1];
|
|
126
|
+
if (!conversationId) {
|
|
127
|
+
stderr(`Conversation id is required.\n\n${usage()}`);
|
|
128
|
+
return 2;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let parsed = null;
|
|
132
|
+
if (command === 'get' || command === 'export') {
|
|
133
|
+
try { parsed = parseOptions(argv); }
|
|
134
|
+
catch (error) {
|
|
135
|
+
stderr(`${error.message}\n\n${usage()}`);
|
|
136
|
+
return 2;
|
|
137
|
+
}
|
|
138
|
+
} else if (argv.length !== 2) {
|
|
139
|
+
stderr(`Unknown option for ${command}\n\n${usage()}`);
|
|
140
|
+
return 2;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const runtime = options.runtime || require('./index');
|
|
145
|
+
if (command === 'stream-status') {
|
|
146
|
+
const state = await runtime.conversation.streamStatus(conversationId);
|
|
147
|
+
stdout(`status=${state?.status || '-'}\nconversation_id=${state?.conversation_id || conversationId}`);
|
|
148
|
+
return 0;
|
|
149
|
+
}
|
|
150
|
+
if (command === 'retry' || command === 'resume') {
|
|
151
|
+
const recovered = await runtime.conversation.retry(conversationId);
|
|
152
|
+
stdout(`status=READY\nconversation_id=${recovered?.conversation_id || recovered?.id || conversationId}\ncurrent_node=${recovered?.current_node || '-'}`);
|
|
98
153
|
return 0;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
154
|
+
}
|
|
155
|
+
const conversation = await runtime.conversation.get(conversationId);
|
|
156
|
+
const rendered = parsed.json
|
|
157
|
+
? formatter.json(conversation)
|
|
158
|
+
: formatter.markdown(conversation, { debug: parsed.debug, limit: parsed.limit });
|
|
159
|
+
|
|
160
|
+
if (command === 'get') {
|
|
161
|
+
stdout(rendered.trimEnd());
|
|
162
|
+
return 0;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const cwd = options.cwd || process.cwd();
|
|
166
|
+
const target = parsed.out
|
|
167
|
+
? path.resolve(cwd, parsed.out)
|
|
168
|
+
: defaultExportPath(conversationId, parsed, cwd);
|
|
169
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
170
|
+
fs.writeFileSync(target, rendered, 'utf8');
|
|
171
|
+
stdout(`exported=${target}`);
|
|
172
|
+
return 0;
|
|
173
|
+
} catch (error) {
|
|
174
|
+
stderr(`${error?.code ? `${error.code}: ` : ''}${error?.message || String(error)}`);
|
|
175
|
+
return 1;
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
if (require.main === module) {
|
|
180
|
+
exports.run(process.argv.slice(2)).then((code) => { process.exitCode = code; });
|
|
181
|
+
}
|
package/src/config/paths.js
CHANGED
|
@@ -9,6 +9,8 @@ exports.stateDir = (env = process.env) => path.resolve(env.ZERO_CHATGPT_STATE_DI
|
|
|
9
9
|
exports.authFile = (env = process.env) => path.resolve(env.ZERO_CHATGPT_AUTH_FILE || path.join(exports.stateDir(env), 'auth-context.json'));
|
|
10
10
|
exports.sessionFile = (env = process.env) => path.resolve(env.ZERO_CHATGPT_SESSION_FILE || path.join(exports.stateDir(env), 'session-context.json'));
|
|
11
11
|
exports.tempConversationFile = (env = process.env) => path.resolve(env.ZERO_CHATGPT_TEMP_STATE_FILE || path.join(exports.stateDir(env), 'temp-conversations.json'));
|
|
12
|
+
exports.conversationContextFile = (env = process.env) => path.resolve(env.ZERO_CHATGPT_CONVERSATION_CONTEXT_FILE || path.join(exports.stateDir(env), 'conversation-context.json'));
|
|
13
|
+
exports.conversationRecoveryFile = (env = process.env) => path.resolve(env.ZERO_CHATGPT_RECOVERY_STATE_FILE || path.join(exports.stateDir(env), 'conversation-recovery.json'));
|
|
12
14
|
exports.rateLimitDir = (env = process.env) => env.ZERO_CHATGPT_RATE_LIMIT_FILE
|
|
13
15
|
? path.dirname(path.resolve(env.ZERO_CHATGPT_RATE_LIMIT_FILE))
|
|
14
16
|
: exports.stateDir(env);
|
|
@@ -16,4 +18,4 @@ exports.rateLimitFile = (env = process.env, token = 'anonymous') => {
|
|
|
16
18
|
if (env.ZERO_CHATGPT_RATE_LIMIT_FILE) return path.resolve(env.ZERO_CHATGPT_RATE_LIMIT_FILE);
|
|
17
19
|
const key = crypto.createHash('sha256').update(String(token || 'anonymous')).digest('hex').slice(0, 16);
|
|
18
20
|
return path.join(exports.rateLimitDir(env), `rate-limit-${key}.json`);
|
|
19
|
-
};
|
|
21
|
+
};
|
package/src/connector/read.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const direct = require('../transport/direct/http-client');
|
|
4
|
+
const context = require('../runtime-context');
|
|
4
5
|
|
|
5
6
|
const requireOk = (response, target) => {
|
|
6
7
|
if (!response || response.status !== 200 || !response.json) {
|
|
@@ -14,9 +15,11 @@ const requireOk = (response, target) => {
|
|
|
14
15
|
return response.json;
|
|
15
16
|
};
|
|
16
17
|
|
|
17
|
-
exports.listConnectors = async (
|
|
18
|
+
exports.listConnectors = async (options = {}) => {
|
|
19
|
+
options = await context.bind(options);
|
|
20
|
+
const requestJson = options.requestJson || direct.requestJson;
|
|
18
21
|
const target = '/backend-api/ps/plugins/installed?limit=1000';
|
|
19
|
-
const raw = requireOk(await requestJson(target, token, sessionHeaders, { route: '/backend-api/ps/plugins/installed', headers: { connection: 'close' } }), target);
|
|
22
|
+
const raw = requireOk(await requestJson(target, options.token, options.sessionHeaders, { route: '/backend-api/ps/plugins/installed', headers: { connection: 'close' } }), target);
|
|
20
23
|
const plugins = Array.isArray(raw?.plugins) ? raw.plugins : [];
|
|
21
24
|
return plugins.map((plugin) => ({
|
|
22
25
|
id: plugin?.id || null,
|
|
@@ -29,10 +32,13 @@ exports.listConnectors = async ({ token, sessionHeaders, requestJson = direct.re
|
|
|
29
32
|
}));
|
|
30
33
|
};
|
|
31
34
|
|
|
32
|
-
exports.getConnectorActions = async (
|
|
35
|
+
exports.getConnectorActions = async (options = {}) => {
|
|
36
|
+
options = await context.bind(options);
|
|
37
|
+
const { appId } = options;
|
|
33
38
|
if (!appId || typeof appId !== 'string') throw Object.assign(new Error('appId is required'), { code: 'BAD_ARGS' });
|
|
39
|
+
const requestJson = options.requestJson || direct.requestJson;
|
|
34
40
|
const target = `/backend-api/aip/connectors/${encodeURIComponent(appId)}/actions`;
|
|
35
|
-
return requireOk(await requestJson(target, token, sessionHeaders, { route: '/backend-api/aip/connectors/{app_id}/actions', headers: { connection: 'close' } }), target);
|
|
41
|
+
return requireOk(await requestJson(target, options.token, options.sessionHeaders, { route: '/backend-api/aip/connectors/{app_id}/actions', headers: { connection: 'close' } }), target);
|
|
36
42
|
};
|
|
37
43
|
|
|
38
44
|
exports.resolveSystemHintSelections = async ({ message, token, sessionHeaders, listConnectors = exports.listConnectors }) => {
|
|
@@ -53,4 +59,22 @@ exports.resolveSystemHintSelections = async ({ message, token, sessionHeaders, l
|
|
|
53
59
|
}
|
|
54
60
|
}
|
|
55
61
|
return { systemHints: [...hints], systemHintMentions };
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
exports.getSystemHints = async (options = {}) => {
|
|
65
|
+
options = await context.bind(options);
|
|
66
|
+
const normalizedMode = String(options.mode || 'plugins').trim();
|
|
67
|
+
if (!normalizedMode) throw Object.assign(new Error('mode is required'), { code: 'BAD_ARGS' });
|
|
68
|
+
const requestJson = options.requestJson || direct.requestJson;
|
|
69
|
+
const target = `/backend-api/system_hints?mode=${encodeURIComponent(normalizedMode)}`;
|
|
70
|
+
return requireOk(await requestJson(target, options.token, options.sessionHeaders, { route: '/backend-api/system_hints', headers: { connection: 'close' } }), target);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
exports.getCanonicalApp = async (options = {}) => {
|
|
74
|
+
options = await context.bind(options);
|
|
75
|
+
const { appId } = options;
|
|
76
|
+
if (!appId || typeof appId !== 'string') throw Object.assign(new Error('appId is required'), { code: 'BAD_ARGS' });
|
|
77
|
+
const requestJson = options.requestJson || direct.requestJson;
|
|
78
|
+
const target = `/backend-api/ps/plugins/canonical-app/${encodeURIComponent(appId)}`;
|
|
79
|
+
return requireOk(await requestJson(target, options.token, options.sessionHeaders, { route: '/backend-api/ps/plugins/canonical-app/{app_id}', headers: { connection: 'close' } }), target);
|
|
56
80
|
};
|
package/src/connector/write.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const direct = require('../transport/direct/http-client');
|
|
4
|
+
const context = require('../runtime-context');
|
|
4
5
|
|
|
5
6
|
const requireSuccess = (response, target) => {
|
|
6
7
|
if (!response || response.status < 200 || response.status >= 300) {
|
|
@@ -14,13 +15,12 @@ const requireSuccess = (response, target) => {
|
|
|
14
15
|
return response.json || { success: true };
|
|
15
16
|
};
|
|
16
17
|
|
|
17
|
-
exports.refreshConnectorActions = async (
|
|
18
|
-
|
|
18
|
+
exports.refreshConnectorActions = async (options = {}) => {
|
|
19
|
+
options = await context.bind(options);
|
|
20
|
+
if (!options.linkId || typeof options.linkId !== 'string') throw Object.assign(new Error('linkId is required'), { code: 'BAD_ARGS' });
|
|
19
21
|
const target = '/backend-api/aip/connectors/mcp/refresh_actions';
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
body: { link_id: linkId },
|
|
23
|
-
route: target,
|
|
24
|
-
retry: false
|
|
22
|
+
const requestJson = options.requestJson || direct.requestJson;
|
|
23
|
+
return requireSuccess(await requestJson(target, options.token, options.sessionHeaders, {
|
|
24
|
+
method: 'POST', body: { link_id: options.linkId }, route: target, retry: false
|
|
25
25
|
}), target);
|
|
26
26
|
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const paths = require('../config/paths');
|
|
5
|
+
|
|
6
|
+
const EMPTY = Object.freeze({
|
|
7
|
+
version: 1,
|
|
8
|
+
enabled: true,
|
|
9
|
+
attach: Object.freeze({ systemHints: [], localFunctionNames: [], systemPrompts: [] })
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const strings = (value, name) => {
|
|
13
|
+
if (value == null) return [];
|
|
14
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== 'string')) {
|
|
15
|
+
throw Object.assign(new TypeError(`${name} must be an array of strings`), { code: 'CONTEXT_CONFIG_INVALID' });
|
|
16
|
+
}
|
|
17
|
+
return [...new Set(value.map((item) => item.trim()).filter(Boolean))];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const normalize = (input = {}) => {
|
|
21
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
22
|
+
throw Object.assign(new TypeError('conversation context config must be an object'), { code: 'CONTEXT_CONFIG_INVALID' });
|
|
23
|
+
}
|
|
24
|
+
const attach = input.attach == null ? {} : input.attach;
|
|
25
|
+
if (!attach || typeof attach !== 'object' || Array.isArray(attach)) {
|
|
26
|
+
throw Object.assign(new TypeError('attach must be an object'), { code: 'CONTEXT_CONFIG_INVALID' });
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
version: 1,
|
|
30
|
+
enabled: input.enabled !== false,
|
|
31
|
+
attach: {
|
|
32
|
+
systemHints: strings(attach.systemHints, 'attach.systemHints'),
|
|
33
|
+
localFunctionNames: strings(attach.localFunctionNames, 'attach.localFunctionNames'),
|
|
34
|
+
systemPrompts: strings(attach.systemPrompts, 'attach.systemPrompts')
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
exports.get = ({ env = process.env } = {}) => {
|
|
40
|
+
const file = paths.conversationContextFile(env);
|
|
41
|
+
if (!fs.existsSync(file)) return normalize(EMPTY);
|
|
42
|
+
try { return normalize(JSON.parse(fs.readFileSync(file, 'utf8'))); }
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (error?.code === 'CONTEXT_CONFIG_INVALID') throw error;
|
|
45
|
+
throw Object.assign(new Error(`invalid conversation context config: ${file}`), { code: 'CONTEXT_CONFIG_INVALID', cause: error });
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
exports.set = (config, { env = process.env } = {}) => {
|
|
50
|
+
const normalized = normalize(config);
|
|
51
|
+
const file = paths.conversationContextFile(env);
|
|
52
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
53
|
+
const temp = `${file}.${process.pid}.tmp`;
|
|
54
|
+
fs.writeFileSync(temp, `${JSON.stringify(normalized, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
55
|
+
fs.renameSync(temp, file);
|
|
56
|
+
return normalized;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const merge = (base, extra) => [...new Set([...(base || []), ...(extra || [])])];
|
|
60
|
+
exports.merge = ({ env = process.env, systemHints = [], localFunctionNames = [], hiddenSystemMessages = [] } = {}) => {
|
|
61
|
+
const config = exports.get({ env });
|
|
62
|
+
if (!config.enabled) return { systemHints, localFunctionNames, hiddenSystemMessages };
|
|
63
|
+
return {
|
|
64
|
+
systemHints: merge(config.attach.systemHints, systemHints),
|
|
65
|
+
localFunctionNames: merge(config.attach.localFunctionNames, localFunctionNames),
|
|
66
|
+
hiddenSystemMessages: merge(config.attach.systemPrompts, hiddenSystemMessages)
|
|
67
|
+
};
|
|
68
|
+
};
|
|
@@ -6,6 +6,7 @@ const paths = require('../config/paths');
|
|
|
6
6
|
const read = require('./read');
|
|
7
7
|
const write = require('./write');
|
|
8
8
|
const temporary = require('./temporary');
|
|
9
|
+
const recovery = require('./recovery');
|
|
9
10
|
|
|
10
11
|
const sessionHeaders = (env) => {
|
|
11
12
|
const file = paths.sessionFile(env);
|
|
@@ -24,7 +25,7 @@ const bindCanonicalContext = async (options = {}) => {
|
|
|
24
25
|
const resolved = { ...options };
|
|
25
26
|
const env = resolved.env || process.env;
|
|
26
27
|
if (!resolved.token) {
|
|
27
|
-
const current = await auth.ensure({ env
|
|
28
|
+
const current = await auth.ensure({ env });
|
|
28
29
|
resolved.token = current.accessToken;
|
|
29
30
|
}
|
|
30
31
|
if (!resolved.sessionHeaders) resolved.sessionHeaders = sessionHeaders(env);
|
|
@@ -36,6 +37,7 @@ exports.get = async (id, options = {}) => read.getConversation({
|
|
|
36
37
|
conversationId: id,
|
|
37
38
|
...(await bindCanonicalContext(options))
|
|
38
39
|
});
|
|
40
|
+
exports.list = async (options = {}) => read.listStandalone(await bindCanonicalContext(options));
|
|
39
41
|
exports.streamStatus = async (id, options = {}) => read.getConversationStreamStatus({
|
|
40
42
|
conversationId: id,
|
|
41
43
|
...(await bindCanonicalContext(options))
|
|
@@ -44,6 +46,10 @@ exports.resume = async (id, options = {}) => read.resumeConversation({
|
|
|
44
46
|
conversationId: id,
|
|
45
47
|
...(await bindCanonicalContext(options))
|
|
46
48
|
});
|
|
49
|
+
exports.retry = async (id, options = {}) => recovery.retry({
|
|
50
|
+
conversationId: id,
|
|
51
|
+
...(await bindCanonicalContext(options))
|
|
52
|
+
});
|
|
47
53
|
exports.send = async (id, message, options = {}) => write.sendConversation({
|
|
48
54
|
conversationId: id,
|
|
49
55
|
message,
|
|
@@ -59,7 +65,7 @@ exports.temp = {
|
|
|
59
65
|
message,
|
|
60
66
|
...(await bindCanonicalContext(options))
|
|
61
67
|
})
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
exports.attachments = require('./attachments');
|
|
65
|
-
exports.model = require('./model');
|
|
68
|
+
};
|
|
69
|
+
exports.context = require('./context-config');
|
|
70
|
+
exports.attachments = require('./attachments');
|
|
71
|
+
exports.model = require('./model');
|
package/src/conversation/read.js
CHANGED
|
@@ -106,15 +106,19 @@ exports.getConversation = async ({
|
|
|
106
106
|
|
|
107
107
|
exports.getConversationStreamStatus = async ({
|
|
108
108
|
conversationId,
|
|
109
|
+
turnTraceId = null,
|
|
109
110
|
token,
|
|
110
111
|
sessionHeaders,
|
|
111
112
|
requestJson = direct.requestJson
|
|
112
113
|
}) => {
|
|
113
114
|
if (!conversationId) throw new Error('conversationId is required');
|
|
114
115
|
const target = `/backend-api/conversation/${encodeURIComponent(conversationId)}/stream_status`;
|
|
115
|
-
|
|
116
|
-
route: '/backend-api/conversation/{conversation_id}/stream_status'
|
|
117
|
-
|
|
116
|
+
const response = await requestJson(target, token, sessionHeaders, {
|
|
117
|
+
route: '/backend-api/conversation/{conversation_id}/stream_status',
|
|
118
|
+
...(turnTraceId ? { headers: { 'x-oai-turn-trace-id': turnTraceId } } : {})
|
|
119
|
+
});
|
|
120
|
+
if (response?.status === 404) return { status: 'UNAVAILABLE', conversation_id: conversationId };
|
|
121
|
+
return requireOk(response, target);
|
|
118
122
|
};
|
|
119
123
|
|
|
120
124
|
exports.resumeConversation = async ({
|
|
@@ -129,6 +133,9 @@ exports.resumeConversation = async ({
|
|
|
129
133
|
}) => {
|
|
130
134
|
if (!conversationId) throw new Error('conversationId is required');
|
|
131
135
|
if (!Number.isSafeInteger(offset) || offset < 0) throw new Error('offset must be a non-negative integer');
|
|
136
|
+
if (offset > 0 && !conduitToken) {
|
|
137
|
+
throw Object.assign(new Error('resume token required after stream progress'), { code: 'STREAM_RESUME_TOKEN_REQUIRED' });
|
|
138
|
+
}
|
|
132
139
|
const target = '/backend-api/f/conversation/resume';
|
|
133
140
|
const response = await requestText(target, token, sessionHeaders, {
|
|
134
141
|
method: 'POST',
|
|
@@ -0,0 +1,51 @@
|
|
|
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 empty = () => ({ version: 1, items: {} });
|
|
8
|
+
const readState = (env = process.env) => {
|
|
9
|
+
try {
|
|
10
|
+
const parsed = JSON.parse(fs.readFileSync(paths.conversationRecoveryFile(env), 'utf8'));
|
|
11
|
+
return parsed && parsed.items && typeof parsed.items === 'object' ? parsed : empty();
|
|
12
|
+
} catch { return empty(); }
|
|
13
|
+
};
|
|
14
|
+
const writeState = (state, env = process.env) => {
|
|
15
|
+
const file = paths.conversationRecoveryFile(env);
|
|
16
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
17
|
+
const temp = `${file}.${process.pid}.tmp`;
|
|
18
|
+
fs.writeFileSync(temp, `${JSON.stringify(state, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
19
|
+
fs.renameSync(temp, file);
|
|
20
|
+
return file;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
exports.get = (conversationId, env = process.env) => {
|
|
24
|
+
const value = readState(env).items[conversationId];
|
|
25
|
+
return value ? structuredClone(value) : null;
|
|
26
|
+
};
|
|
27
|
+
exports.save = ({ conversationId, phase = 'stream', offset = 0, resumeToken = null, turnTraceId = null, parentMessageId = null, currentNode = null }, env = process.env) => {
|
|
28
|
+
if (!conversationId) throw new TypeError('conversation recovery requires conversationId');
|
|
29
|
+
if (!Number.isSafeInteger(offset) || offset < 0) throw new TypeError('conversation recovery offset must be a non-negative integer');
|
|
30
|
+
const state = readState(env);
|
|
31
|
+
state.items[conversationId] = {
|
|
32
|
+
conversation_id: conversationId,
|
|
33
|
+
phase,
|
|
34
|
+
offset,
|
|
35
|
+
resume_token: typeof resumeToken === 'string' && resumeToken ? resumeToken : null,
|
|
36
|
+
turn_trace_id: typeof turnTraceId === 'string' && turnTraceId ? turnTraceId : null,
|
|
37
|
+
parent_message_id: typeof parentMessageId === 'string' && parentMessageId ? parentMessageId : null,
|
|
38
|
+
current_node: typeof currentNode === 'string' && currentNode ? currentNode : null,
|
|
39
|
+
updated_at: new Date().toISOString()
|
|
40
|
+
};
|
|
41
|
+
writeState(state, env);
|
|
42
|
+
return exports.get(conversationId, env);
|
|
43
|
+
};
|
|
44
|
+
exports.remove = (conversationId, env = process.env) => {
|
|
45
|
+
const state = readState(env);
|
|
46
|
+
const existed = Boolean(state.items[conversationId]);
|
|
47
|
+
if (!existed) return false;
|
|
48
|
+
delete state.items[conversationId];
|
|
49
|
+
writeState(state, env);
|
|
50
|
+
return true;
|
|
51
|
+
};
|