@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/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 get <conversation-id> [--limit N|A-B] [--debug|--json]',
13
- ' zgpt export <conversation-id> [--limit N|A-B] [--debug|--json] [--out <path>]',
14
- ' zgpt help'
15
- ].join('\n');
16
-
17
- const parseOptions = (argv) => {
18
- const result = { json: false, debug: false, limit: null, out: null };
19
- for (let index = 2; index < argv.length; index += 1) {
20
- const arg = argv[index];
21
- if (arg === '--json' || arg === 'json') result.json = true;
22
- else if (arg === '--debug' || arg === 'debug') result.debug = true;
23
- else if (arg === '--limit' || arg === 'limit') {
24
- const value = argv[++index];
25
- result.limit = formatter.parseLimitSpec(value);
26
- if (!result.limit) throw new Error(`Invalid limit: ${value || '(missing)'}`);
27
- } else if (arg === '--out' || arg === 'out') {
28
- result.out = argv[++index] || null;
29
- if (!result.out) throw new Error('Export path is required after --out');
30
- } else throw new Error(`Unknown option: ${arg}`);
31
- }
32
- if (result.json && result.debug) throw new Error('--json and --debug are mutually exclusive');
33
- return result;
34
- };
35
-
36
- const defaultExportPath = (conversationId, options, cwd) => {
37
- const safeId = String(conversationId).replace(/[^a-zA-Z0-9._-]/g, '_');
38
- const suffix = options.json ? '.json' : options.debug ? '.debug.md' : '.md';
39
- return path.join(cwd, 'exports', `conversation-${safeId}${suffix}`);
40
- };
41
-
42
- exports.run = async (argv = [], options = {}) => {
43
- const stdout = options.stdout || console.log;
44
- const stderr = options.stderr || console.error;
45
- const command = argv[0] || 'help';
46
- if (command === 'help' || command === '--help' || command === '-h') {
47
- stdout(usage());
48
- return 0;
49
- }
50
- if (!['get', 'export', 'stream-status', 'resume'].includes(command)) {
51
- stderr(`Unknown command: ${command}\n\n${usage()}`);
52
- return 2;
53
- }
54
- const conversationId = argv[1];
55
- if (!conversationId) {
56
- stderr(`Conversation id is required.\n\n${usage()}`);
57
- return 2;
58
- }
59
-
60
- let parsed = null;
61
- if (command === 'get' || command === 'export') {
62
- try { parsed = parseOptions(argv); }
63
- catch (error) {
64
- stderr(`${error.message}\n\n${usage()}`);
65
- return 2;
66
- }
67
- } else if (argv.length !== 2) {
68
- stderr(`Unknown option for ${command}\n\n${usage()}`);
69
- return 2;
70
- }
71
-
72
- try {
73
- const runtime = options.runtime || require('./index');
74
- if (command === 'stream-status') {
75
- const state = await runtime.conversation.streamStatus(conversationId);
76
- stdout(`status=${state?.status || '-'}\nconversation_id=${state?.conversation_id || conversationId}`);
77
- return 0;
78
- }
79
- if (command === 'resume') {
80
- const state = await runtime.conversation.streamStatus(conversationId);
81
- if (state?.status !== 'IS_STREAMING') {
82
- stdout(`status=${state?.status || '-'}\nconversation_id=${conversationId}\nresume=SKIPPED`);
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 resumed = await runtime.conversation.resume(conversationId);
86
- let text = `status=${state.status}\nconversation_id=${conversationId}\nresume=OK\nhttp_status=${resumed?.status ?? '-'}\ncontent_type=${resumed?.content_type || '-'}`;
87
- if (resumed?.text) text += `\n${String(resumed.text).trimEnd()}`;
88
- stdout(text);
89
- return 0;
90
- }
91
- const conversation = await runtime.conversation.get(conversationId);
92
- const rendered = parsed.json
93
- ? formatter.json(conversation)
94
- : formatter.markdown(conversation, { debug: parsed.debug, limit: parsed.limit });
95
-
96
- if (command === 'get') {
97
- stdout(rendered.trimEnd());
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 cwd = options.cwd || process.cwd();
102
- const target = parsed.out
103
- ? path.resolve(cwd, parsed.out)
104
- : defaultExportPath(conversationId, parsed, cwd);
105
- fs.mkdirSync(path.dirname(target), { recursive: true });
106
- fs.writeFileSync(target, rendered, 'utf8');
107
- stdout(`exported=${target}`);
108
- return 0;
109
- } catch (error) {
110
- stderr(`${error?.code ? `${error.code}: ` : ''}${error?.message || String(error)}`);
111
- return 1;
112
- }
113
- };
114
-
115
- if (require.main === module) {
116
- exports.run(process.argv.slice(2)).then((code) => { process.exitCode = code; });
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
+ }
@@ -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
+ };
@@ -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 ({ token, sessionHeaders, requestJson = direct.requestJson }) => {
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 ({ appId, token, sessionHeaders, requestJson = direct.requestJson }) => {
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
  };
@@ -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 ({ linkId, token, sessionHeaders, requestJson = direct.requestJson }) => {
18
- if (!linkId || typeof linkId !== 'string') throw Object.assign(new Error('linkId is required'), { code: 'BAD_ARGS' });
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
- return requireSuccess(await requestJson(target, token, sessionHeaders, {
21
- method: 'POST',
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, acquireSession: false });
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');
@@ -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
- return requireOk(await requestJson(target, token, sessionHeaders, {
116
- route: '/backend-api/conversation/{conversation_id}/stream_status'
117
- }), target);
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
+ };