agentgui 1.0.916 → 1.0.918
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/CLAUDE.md +1 -382
- package/database-schema.js +0 -58
- package/lib/db-queries-cleanup.js +0 -12
- package/lib/db-queries-del.js +0 -1
- package/lib/db-queries.js +0 -4
- package/lib/http-handler.js +1 -10
- package/lib/plugins/database-plugin.js +1 -1
- package/lib/process-message.js +2 -2
- package/lib/provider-config.js +0 -16
- package/lib/recovery.js +2 -12
- package/lib/routes-agent-actions.js +2 -58
- package/lib/routes-debug.js +1 -7
- package/lib/routes-registry.js +5 -17
- package/lib/server-startup.js +1 -59
- package/lib/stream-event-handler.js +1 -3
- package/lib/ws-handlers-session2.js +2 -23
- package/lib/ws-handlers-util.js +106 -175
- package/lib/ws-legacy-handlers.js +1 -104
- package/lib/ws-setup.js +1 -3
- package/package.json +1 -15
- package/server.js +9 -26
- package/test.js +1 -21
- package/ecosystem.config.cjs +0 -22
- package/lib/checkpoint-manager.js +0 -182
- package/lib/db-queries-tools.js +0 -131
- package/lib/db-queries-voice.js +0 -85
- package/lib/oauth-codex.js +0 -164
- package/lib/oauth-common.js +0 -92
- package/lib/oauth-gemini.js +0 -199
- package/lib/plugins/auth-plugin.js +0 -132
- package/lib/plugins/speech-plugin.js +0 -72
- package/lib/plugins/tools-plugin.js +0 -120
- package/lib/pm2-manager.js +0 -170
- package/lib/routes-oauth.js +0 -105
- package/lib/routes-speech.js +0 -173
- package/lib/routes-tools.js +0 -184
- package/lib/speech-manager.js +0 -200
- package/lib/speech.js +0 -50
- package/lib/tool-install-machine.js +0 -157
- package/lib/tool-manager.js +0 -98
- package/lib/tool-provisioner.js +0 -98
- package/lib/tool-spawner.js +0 -163
- package/lib/tool-version-check.js +0 -196
- package/lib/tool-version-fetch.js +0 -68
- package/lib/ws-handlers-oauth.js +0 -76
- package/static/js/agent-auth-oauth.js +0 -159
- package/static/js/pm2-monitor.js +0 -151
- package/static/js/stt-handler.js +0 -147
- package/static/js/tool-install-machine.js +0 -155
- package/static/js/tools-manager-ui.js +0 -124
- package/static/js/tools-manager.js +0 -172
- package/static/js/voice-machine.js +0 -145
- package/static/js/voice.js +0 -134
package/test.js
CHANGED
|
@@ -7,12 +7,9 @@ import { createQueries } from './lib/db-queries.js';
|
|
|
7
7
|
import { encode, decode } from './lib/codec.js';
|
|
8
8
|
import { WsRouter } from './lib/ws-protocol.js';
|
|
9
9
|
import { WSOptimizer } from './lib/ws-optimizer.js';
|
|
10
|
-
import * as tim from './lib/tool-install-machine.js';
|
|
11
10
|
import * as exm from './lib/execution-machine.js';
|
|
12
11
|
import * as asm from './lib/acp-server-machine.js';
|
|
13
12
|
import { maskKey, buildSystemPrompt } from './lib/provider-config.js';
|
|
14
|
-
import { buildBaseUrl, isRemoteRequest, encodeOAuthState, decodeOAuthState } from './lib/oauth-common.js';
|
|
15
|
-
import { compareVersions } from './lib/tool-version-check.js';
|
|
16
13
|
import { initializeDescriptors, getAgentDescriptor } from './lib/agent-descriptors.js';
|
|
17
14
|
import { createACPProtocolHandler } from './lib/acp-protocol.js';
|
|
18
15
|
import { sendJSON, compressAndSend, acceptsEncoding } from './lib/http-utils.js';
|
|
@@ -87,8 +84,7 @@ await ok('WsRouter: dispatch + 404 + error + legacy', async () => {
|
|
|
87
84
|
assert.deepEqual(replies[0], { r: 1, d: { pong: 7 } });
|
|
88
85
|
assert.equal(replies[1].e.c, 404); assert.equal(replies[2].e.c, 422); assert.equal(legacy.type, 'subscribe');
|
|
89
86
|
});
|
|
90
|
-
await ok('machines:
|
|
91
|
-
assert.ok(tim.getMachineActors() instanceof Map);
|
|
87
|
+
await ok('machines: execution + acp-server lifecycle', () => {
|
|
92
88
|
assert.equal(exm.snapshot('nonexistent-conv-id'), null);
|
|
93
89
|
assert.equal(asm.getOrCreate('test-tool').getSnapshot().value, 'stopped');
|
|
94
90
|
asm.send('test-tool', { type: 'START', pid: 123 });
|
|
@@ -122,22 +118,6 @@ await ok('provider-config: maskKey + buildSystemPrompt', () => {
|
|
|
122
118
|
assert.equal(buildSystemPrompt('opencode', 'sonnet'), 'Use opencode subagent for all tasks. Model: sonnet.');
|
|
123
119
|
assert.equal(buildSystemPrompt('foo-·-bar', null, 'sub'), 'Use foo subagent for all tasks. Subagent: sub.');
|
|
124
120
|
});
|
|
125
|
-
await ok('oauth-common: state codec + url helpers', () => {
|
|
126
|
-
const dec = decodeOAuthState(encodeOAuthState('csrf-tok', 'https://relay.test/cb'));
|
|
127
|
-
assert.equal(dec.csrfToken, 'csrf-tok'); assert.equal(dec.relayUrl, 'https://relay.test/cb');
|
|
128
|
-
const fb = decodeOAuthState('not-base64-json');
|
|
129
|
-
assert.equal(fb.csrfToken, 'not-base64-json'); assert.equal(fb.relayUrl, null);
|
|
130
|
-
const reqRemote = { headers: { 'x-forwarded-host': 'a.com', 'x-forwarded-proto': 'https' }, socket: {} };
|
|
131
|
-
assert.equal(buildBaseUrl(reqRemote, 3000), 'https://a.com'); assert.equal(isRemoteRequest(reqRemote), true);
|
|
132
|
-
assert.equal(buildBaseUrl({ headers: {}, socket: {} }, 3000), 'http://127.0.0.1:3000');
|
|
133
|
-
assert.equal(isRemoteRequest({ headers: {} }), false);
|
|
134
|
-
});
|
|
135
|
-
await ok('tool-version-check: compareVersions', () => {
|
|
136
|
-
assert.equal(compareVersions('1.0.0', '1.0.1'), true); assert.equal(compareVersions('1.0.1', '1.0.0'), false);
|
|
137
|
-
assert.equal(compareVersions('1.0.0', '1.0.0'), false); assert.equal(compareVersions('1.2', '1.2.1'), true);
|
|
138
|
-
assert.equal(compareVersions('2.0.0', '1.99.99'), false);
|
|
139
|
-
assert.equal(compareVersions(null, '1.0.0'), false); assert.equal(compareVersions('1.0.0', null), false);
|
|
140
|
-
});
|
|
141
121
|
await ok('agent-descriptors: initialize + cache', () => {
|
|
142
122
|
assert.equal(initializeDescriptors([{ id: 'claude-code', name: 'Claude Code', path: '/x' }]), 1);
|
|
143
123
|
const d = getAgentDescriptor('claude-code');
|
package/ecosystem.config.cjs
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
module.exports = {
|
|
2
|
-
apps: [{
|
|
3
|
-
name: 'agentgui',
|
|
4
|
-
script: 'server.js',
|
|
5
|
-
interpreter: 'node',
|
|
6
|
-
interpreter_args: '--experimental-vm-modules',
|
|
7
|
-
watch: false,
|
|
8
|
-
env: {
|
|
9
|
-
NODE_ENV: 'development',
|
|
10
|
-
PORT: '3000',
|
|
11
|
-
HOT_RELOAD: 'false'
|
|
12
|
-
},
|
|
13
|
-
max_memory_restart: '512M',
|
|
14
|
-
restart_delay: 2000,
|
|
15
|
-
max_restarts: 10,
|
|
16
|
-
exp_backoff_restart_delay: 100,
|
|
17
|
-
error_file: '~/.gmgui/logs/err.log',
|
|
18
|
-
out_file: '~/.gmgui/logs/out.log',
|
|
19
|
-
merge_logs: true,
|
|
20
|
-
time: true
|
|
21
|
-
}]
|
|
22
|
-
};
|
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Checkpoint Manager
|
|
3
|
-
* Handles session recovery by loading checkpoints and injecting into resume flow
|
|
4
|
-
* Ensures idempotency and prevents duplicate event replay
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
class CheckpointManager {
|
|
8
|
-
constructor(queries) {
|
|
9
|
-
this.queries = queries;
|
|
10
|
-
this._injectedSessions = new Set(); // Track which sessions already had checkpoints injected
|
|
11
|
-
this._pendingCheckpoints = new Map(); // Store checkpoints to inject on subscribe: { conversationId -> checkpoint }
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Load checkpoint for a session (all events + chunks from previous session)
|
|
16
|
-
* Used when resuming after interruption
|
|
17
|
-
*/
|
|
18
|
-
loadCheckpoint(previousSessionId) {
|
|
19
|
-
if (!previousSessionId) return null;
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
const session = this.queries.getSession(previousSessionId);
|
|
23
|
-
if (!session) return null;
|
|
24
|
-
|
|
25
|
-
const events = this.queries.getSessionEvents(previousSessionId);
|
|
26
|
-
const chunks = this.queries.getChunksSinceSeq(previousSessionId, -1);
|
|
27
|
-
|
|
28
|
-
return {
|
|
29
|
-
sessionId: previousSessionId,
|
|
30
|
-
conversationId: session.conversationId,
|
|
31
|
-
events: events || [],
|
|
32
|
-
chunks: chunks || [],
|
|
33
|
-
lastSequence: chunks.length > 0 ? Math.max(...chunks.map(c => c.sequence)) : -1
|
|
34
|
-
};
|
|
35
|
-
} catch (e) {
|
|
36
|
-
console.error(`[checkpoint] Failed to load checkpoint for session ${previousSessionId}:`, e.message);
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Inject checkpoint events into the new session
|
|
43
|
-
* Marks them with resumeOrigin to prevent replay
|
|
44
|
-
* Returns the next sequence number to use
|
|
45
|
-
*/
|
|
46
|
-
injectCheckpointEvents(newSessionId, checkpoint, broadcastFn) {
|
|
47
|
-
if (!checkpoint || !checkpoint.events || checkpoint.events.length === 0) {
|
|
48
|
-
return -1;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Prevent double-injection for same session
|
|
52
|
-
const injectionKey = `${newSessionId}:checkpoint`;
|
|
53
|
-
if (this._injectedSessions.has(injectionKey)) {
|
|
54
|
-
console.log(`[checkpoint] Session ${newSessionId} already had checkpoint injected, skipping`);
|
|
55
|
-
return checkpoint.lastSequence;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
let sequenceStart = checkpoint.lastSequence + 1;
|
|
59
|
-
|
|
60
|
-
try {
|
|
61
|
-
// Broadcast each checkpoint event as if it's arriving now
|
|
62
|
-
for (const evt of checkpoint.events) {
|
|
63
|
-
// Skip internal session management events
|
|
64
|
-
if (evt.type === 'session.created') continue;
|
|
65
|
-
|
|
66
|
-
// Re-broadcast with resume markers
|
|
67
|
-
broadcastFn({
|
|
68
|
-
...evt,
|
|
69
|
-
resumeOrigin: 'checkpoint',
|
|
70
|
-
originalSessionId: checkpoint.sessionId,
|
|
71
|
-
newSessionId: newSessionId,
|
|
72
|
-
timestamp: Date.now()
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Mark this session as having been injected
|
|
77
|
-
this._injectedSessions.add(injectionKey);
|
|
78
|
-
|
|
79
|
-
console.log(
|
|
80
|
-
`[checkpoint] Injected ${checkpoint.events.length} events from session ` +
|
|
81
|
-
`${checkpoint.sessionId} into new session ${newSessionId}`
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
return sequenceStart;
|
|
85
|
-
} catch (e) {
|
|
86
|
-
console.error(`[checkpoint] Failed to inject checkpoint events:`, e.message);
|
|
87
|
-
return checkpoint.lastSequence;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Copy checkpoint chunks to new session with modified sequence
|
|
93
|
-
* Ensures chunks are marked as injected to distinguish from new streaming
|
|
94
|
-
*/
|
|
95
|
-
copyCheckpointChunks(oldSessionId, newSessionId, startSequence = 0) {
|
|
96
|
-
try {
|
|
97
|
-
const chunks = this.queries.getChunksSinceSeq(oldSessionId, -1);
|
|
98
|
-
if (!chunks || chunks.length === 0) return 0;
|
|
99
|
-
|
|
100
|
-
let copiedCount = 0;
|
|
101
|
-
|
|
102
|
-
for (let i = 0; i < chunks.length; i++) {
|
|
103
|
-
const chunk = chunks[i];
|
|
104
|
-
const newSequence = startSequence + i;
|
|
105
|
-
|
|
106
|
-
try {
|
|
107
|
-
this.queries.createChunk(
|
|
108
|
-
newSessionId,
|
|
109
|
-
chunk.conversationId,
|
|
110
|
-
newSequence,
|
|
111
|
-
chunk.type,
|
|
112
|
-
{ ...chunk.data, resumeOrigin: 'checkpoint' }
|
|
113
|
-
);
|
|
114
|
-
copiedCount++;
|
|
115
|
-
} catch (e) {
|
|
116
|
-
console.error(`[checkpoint] Failed to copy chunk ${i}:`, e.message);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
console.log(`[checkpoint] Copied ${copiedCount} chunks from ${oldSessionId} to ${newSessionId}`);
|
|
121
|
-
return startSequence + copiedCount;
|
|
122
|
-
} catch (e) {
|
|
123
|
-
console.error(`[checkpoint] Failed to copy checkpoint chunks:`, e.message);
|
|
124
|
-
return startSequence;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Clean up: mark previous session as properly resumed
|
|
130
|
-
* Prevents re-resuming the same interrupted session multiple times
|
|
131
|
-
*/
|
|
132
|
-
markSessionResumed(previousSessionId) {
|
|
133
|
-
try {
|
|
134
|
-
this.queries.updateSession(previousSessionId, {
|
|
135
|
-
status: 'resumed',
|
|
136
|
-
completed_at: Date.now()
|
|
137
|
-
});
|
|
138
|
-
console.log(`[checkpoint] Marked session ${previousSessionId} as resumed`);
|
|
139
|
-
} catch (e) {
|
|
140
|
-
console.error(`[checkpoint] Failed to mark session as resumed:`, e.message);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Clear injected sessions cache (call on server restart)
|
|
146
|
-
*/
|
|
147
|
-
reset() {
|
|
148
|
-
this._injectedSessions.clear();
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Store a checkpoint to be injected when client subscribes
|
|
153
|
-
* (Used during server startup when no clients are connected yet)
|
|
154
|
-
*/
|
|
155
|
-
storeCheckpointForDelay(conversationId, checkpoint) {
|
|
156
|
-
if (checkpoint && checkpoint.events && checkpoint.events.length > 0) {
|
|
157
|
-
this._pendingCheckpoints.set(conversationId, checkpoint);
|
|
158
|
-
console.log(`[checkpoint] Stored checkpoint for ${conversationId} to inject on subscribe (${checkpoint.events.length} events, ${checkpoint.chunks.length} chunks)`);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Get and remove a pending checkpoint (called when client subscribes)
|
|
164
|
-
*/
|
|
165
|
-
getPendingCheckpoint(conversationId) {
|
|
166
|
-
const checkpoint = this._pendingCheckpoints.get(conversationId);
|
|
167
|
-
if (checkpoint) {
|
|
168
|
-
this._pendingCheckpoints.delete(conversationId);
|
|
169
|
-
console.log(`[checkpoint] Retrieved pending checkpoint for ${conversationId}`);
|
|
170
|
-
}
|
|
171
|
-
return checkpoint;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Check if there's a pending checkpoint for a conversation
|
|
176
|
-
*/
|
|
177
|
-
hasPendingCheckpoint(conversationId) {
|
|
178
|
-
return this._pendingCheckpoints.has(conversationId);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
export default CheckpointManager;
|
package/lib/db-queries-tools.js
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
export function addToolQueries(q, db, prep, generateId) {
|
|
2
|
-
q.initializeToolInstallations = function(tools) {
|
|
3
|
-
const now = Date.now();
|
|
4
|
-
for (const tool of tools) {
|
|
5
|
-
const stmt = prep(`
|
|
6
|
-
INSERT OR IGNORE INTO tool_installations
|
|
7
|
-
(id, tool_id, status, created_at, updated_at)
|
|
8
|
-
VALUES (?, ?, ?, ?, ?)
|
|
9
|
-
`);
|
|
10
|
-
stmt.run(generateId('tinst'), tool.id, 'not_installed', now, now);
|
|
11
|
-
}
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
q.getToolStatus = function(toolId) {
|
|
15
|
-
const stmt = prep(`
|
|
16
|
-
SELECT id, tool_id, version, installed_at, status, last_check_at,
|
|
17
|
-
error_message, update_available, latest_version, created_at, updated_at
|
|
18
|
-
FROM tool_installations
|
|
19
|
-
WHERE tool_id = ?
|
|
20
|
-
`);
|
|
21
|
-
return stmt.get(toolId) || null;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
q.getAllToolStatuses = function() {
|
|
25
|
-
const stmt = prep(`
|
|
26
|
-
SELECT id, tool_id, version, installed_at, status, last_check_at,
|
|
27
|
-
error_message, update_available, latest_version, created_at, updated_at
|
|
28
|
-
FROM tool_installations
|
|
29
|
-
ORDER BY tool_id
|
|
30
|
-
`);
|
|
31
|
-
return stmt.all();
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
q.insertToolInstallation = function(toolId, data) {
|
|
35
|
-
const now = Date.now();
|
|
36
|
-
const stmt = prep(`
|
|
37
|
-
INSERT OR IGNORE INTO tool_installations
|
|
38
|
-
(id, tool_id, version, installed_at, status, last_check_at, error_message, update_available, latest_version, created_at, updated_at)
|
|
39
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
40
|
-
`);
|
|
41
|
-
stmt.run(
|
|
42
|
-
generateId('ti'),
|
|
43
|
-
toolId,
|
|
44
|
-
data.version || null,
|
|
45
|
-
data.installed_at || null,
|
|
46
|
-
data.status || 'not_installed',
|
|
47
|
-
now,
|
|
48
|
-
data.error_message || null,
|
|
49
|
-
0,
|
|
50
|
-
null,
|
|
51
|
-
now,
|
|
52
|
-
now
|
|
53
|
-
);
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
q.updateToolStatus = function(toolId, data) {
|
|
57
|
-
const now = Date.now();
|
|
58
|
-
const stmt = prep(`
|
|
59
|
-
UPDATE tool_installations
|
|
60
|
-
SET version = ?, installed_at = ?, status = ?, last_check_at = ?,
|
|
61
|
-
error_message = ?, update_available = ?, latest_version = ?, updated_at = ?
|
|
62
|
-
WHERE tool_id = ?
|
|
63
|
-
`);
|
|
64
|
-
stmt.run(
|
|
65
|
-
data.version || null,
|
|
66
|
-
data.installed_at || null,
|
|
67
|
-
data.status || 'not_installed',
|
|
68
|
-
data.last_check_at || now,
|
|
69
|
-
data.error_message || null,
|
|
70
|
-
data.update_available ? 1 : 0,
|
|
71
|
-
data.latest_version || null,
|
|
72
|
-
now,
|
|
73
|
-
toolId
|
|
74
|
-
);
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
q.addToolInstallHistory = function(toolId, action, status, error) {
|
|
78
|
-
const now = Date.now();
|
|
79
|
-
// Ensure the parent tool_installations row exists before inserting history
|
|
80
|
-
// (handles tools added after the DB was first initialized)
|
|
81
|
-
prep(`INSERT OR IGNORE INTO tool_installations (id, tool_id, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?)`)
|
|
82
|
-
.run(generateId('tinst'), toolId, 'not_installed', now, now);
|
|
83
|
-
const stmt = prep(`
|
|
84
|
-
INSERT INTO tool_install_history
|
|
85
|
-
(id, tool_id, action, started_at, completed_at, status, error_message, created_at)
|
|
86
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
87
|
-
`);
|
|
88
|
-
stmt.run(generateId('thist'), toolId, action, now, now, status, error || null, now);
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
q.getToolInstallHistory = function(toolId, limit = 20, offset = 0) {
|
|
92
|
-
const stmt = prep(`
|
|
93
|
-
SELECT id, tool_id, action, started_at, completed_at, status, error_message, created_at
|
|
94
|
-
FROM tool_install_history
|
|
95
|
-
WHERE tool_id = ?
|
|
96
|
-
ORDER BY created_at DESC
|
|
97
|
-
LIMIT ? OFFSET ?
|
|
98
|
-
`);
|
|
99
|
-
return stmt.all(toolId, limit, offset);
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
q.pruneToolInstallHistory = function(toolId, keepCount = 100) {
|
|
103
|
-
const stmt = prep(`
|
|
104
|
-
DELETE FROM tool_install_history
|
|
105
|
-
WHERE tool_id = ? AND id NOT IN (
|
|
106
|
-
SELECT id FROM tool_install_history
|
|
107
|
-
WHERE tool_id = ?
|
|
108
|
-
ORDER BY created_at DESC
|
|
109
|
-
LIMIT ?
|
|
110
|
-
)
|
|
111
|
-
`);
|
|
112
|
-
return stmt.run(toolId, toolId, keepCount).changes;
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
q.searchMessages = function(query, limit = 50) {
|
|
116
|
-
try {
|
|
117
|
-
const sanitized = '"' + query.replace(/"/g, '""') + '"';
|
|
118
|
-
const stmt = prep(`
|
|
119
|
-
SELECT m.id, m.conversationId, m.role, m.content, m.created_at,
|
|
120
|
-
c.title as conversationTitle, c.agentType
|
|
121
|
-
FROM messages_fts fts
|
|
122
|
-
JOIN messages m ON m.rowid = fts.rowid
|
|
123
|
-
JOIN conversations c ON c.id = m.conversationId
|
|
124
|
-
WHERE messages_fts MATCH ?
|
|
125
|
-
ORDER BY m.created_at DESC LIMIT ?
|
|
126
|
-
`);
|
|
127
|
-
return stmt.all(sanitized, limit);
|
|
128
|
-
} catch (_) { return []; }
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
}
|
package/lib/db-queries-voice.js
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
export function addVoiceQueries(q, db, prep, generateId) {
|
|
2
|
-
q.getDownloadsByStatus = function(status) {
|
|
3
|
-
const stmt = prep('SELECT * FROM WHERE status = ? ORDER BY started_at DESC');
|
|
4
|
-
return stmt.all(status);
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
q.updateDownloadResume = function(downloadId, currentSize, attempts, lastAttempt, status) {
|
|
8
|
-
const stmt = prep(`
|
|
9
|
-
UPDATE
|
|
10
|
-
SET downloaded_bytes = ?, attempts = ?, lastAttempt = ?, status = ?
|
|
11
|
-
WHERE id = ?
|
|
12
|
-
`);
|
|
13
|
-
stmt.run(currentSize, attempts, lastAttempt, status, downloadId);
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
q.updateDownloadHash = function(downloadId, hash) {
|
|
17
|
-
const stmt = prep('UPDATE SET hash = ? WHERE id = ?');
|
|
18
|
-
stmt.run(hash, downloadId);
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
q.markDownloadResuming = function(downloadId) {
|
|
22
|
-
const stmt = prep('UPDATE SET status = ?, lastAttempt = ? WHERE id = ?');
|
|
23
|
-
stmt.run('resuming', Date.now(), downloadId);
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
q.markDownloadPaused = function(downloadId, errorMessage) {
|
|
27
|
-
const stmt = prep('UPDATE SET status = ?, error_message = ?, lastAttempt = ? WHERE id = ?');
|
|
28
|
-
stmt.run('paused', errorMessage, Date.now(), downloadId);
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
q.saveVoiceCache = function(conversationId, text, audioBlob, ttlMs = 3600000) {
|
|
32
|
-
const id = generateId('vcache');
|
|
33
|
-
const now = Date.now();
|
|
34
|
-
const expiresAt = now + ttlMs;
|
|
35
|
-
const byteSize = audioBlob ? Buffer.byteLength(audioBlob) : 0;
|
|
36
|
-
const stmt = prep(`
|
|
37
|
-
INSERT INTO voice_cache (id, conversationId, text, audioBlob, byteSize, created_at, expires_at)
|
|
38
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
39
|
-
`);
|
|
40
|
-
stmt.run(id, conversationId, text, audioBlob || null, byteSize, now, expiresAt);
|
|
41
|
-
return { id, conversationId, text, byteSize, created_at: now, expires_at: expiresAt };
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
q.getVoiceCache = function(conversationId, text) {
|
|
45
|
-
const now = Date.now();
|
|
46
|
-
const stmt = prep(`
|
|
47
|
-
SELECT id, conversationId, text, audioBlob, byteSize, created_at, expires_at
|
|
48
|
-
FROM voice_cache
|
|
49
|
-
WHERE conversationId = ? AND text = ? AND expires_at > ?
|
|
50
|
-
LIMIT 1
|
|
51
|
-
`);
|
|
52
|
-
return stmt.get(conversationId, text, now) || null;
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
q.cleanExpiredVoiceCache = function() {
|
|
56
|
-
const now = Date.now();
|
|
57
|
-
const stmt = prep('DELETE FROM voice_cache WHERE expires_at <= ?');
|
|
58
|
-
return stmt.run(now).changes;
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
q.getVoiceCacheSize = function(conversationId) {
|
|
62
|
-
const now = Date.now();
|
|
63
|
-
const stmt = prep(`
|
|
64
|
-
SELECT COALESCE(SUM(byteSize), 0) as totalSize
|
|
65
|
-
FROM voice_cache
|
|
66
|
-
WHERE conversationId = ? AND expires_at > ?
|
|
67
|
-
`);
|
|
68
|
-
return stmt.get(conversationId, now).totalSize || 0;
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
q.deleteOldestVoiceCache = function(conversationId, neededBytes) {
|
|
72
|
-
const stmt = prep(`
|
|
73
|
-
SELECT id FROM voice_cache
|
|
74
|
-
WHERE conversationId = ?
|
|
75
|
-
ORDER BY created_at ASC
|
|
76
|
-
LIMIT (SELECT COUNT(*) FROM voice_cache WHERE conversationId = ? AND byteSize > ?)
|
|
77
|
-
`);
|
|
78
|
-
const oldest = stmt.all(conversationId, conversationId, neededBytes);
|
|
79
|
-
const deleteStmt = prep('DELETE FROM voice_cache WHERE id = ?');
|
|
80
|
-
for (const row of oldest) {
|
|
81
|
-
deleteStmt.run(row.id);
|
|
82
|
-
}
|
|
83
|
-
return oldest.length;
|
|
84
|
-
};
|
|
85
|
-
}
|
package/lib/oauth-codex.js
DELETED
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import os from 'os';
|
|
4
|
-
import crypto from 'crypto';
|
|
5
|
-
import { buildBaseUrl, isRemoteRequest, encodeOAuthState, decodeOAuthState, oauthResultPage, oauthRelayPage } from './oauth-common.js';
|
|
6
|
-
|
|
7
|
-
const CODEX_HOME = process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
|
|
8
|
-
const CODEX_AUTH_FILE = path.join(CODEX_HOME, 'auth.json');
|
|
9
|
-
const CODEX_OAUTH_ISSUER = 'https://auth.openai.com';
|
|
10
|
-
const CODEX_CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
|
|
11
|
-
const CODEX_SCOPES = 'openid profile email offline_access api.connectors.read api.connectors.invoke';
|
|
12
|
-
const CODEX_OAUTH_PORT = 1455;
|
|
13
|
-
|
|
14
|
-
let codexOAuthState = { status: 'idle', error: null, email: null };
|
|
15
|
-
let codexOAuthPending = null;
|
|
16
|
-
|
|
17
|
-
function generatePkce() {
|
|
18
|
-
const verifierBytes = crypto.randomBytes(64);
|
|
19
|
-
const codeVerifier = verifierBytes.toString('base64url');
|
|
20
|
-
const challengeBytes = crypto.createHash('sha256').update(codeVerifier).digest();
|
|
21
|
-
const codeChallenge = challengeBytes.toString('base64url');
|
|
22
|
-
return { codeVerifier, codeChallenge };
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function parseJwtEmail(jwt) {
|
|
26
|
-
try {
|
|
27
|
-
const parts = jwt.split('.');
|
|
28
|
-
if (parts.length < 2) return '';
|
|
29
|
-
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
|
|
30
|
-
return payload.email || payload['https://api.openai.com/profile']?.email || '';
|
|
31
|
-
} catch (_) { return ''; }
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function saveCodexCredentials(tokens) {
|
|
35
|
-
if (!fs.existsSync(CODEX_HOME)) fs.mkdirSync(CODEX_HOME, { recursive: true });
|
|
36
|
-
const auth = { auth_mode: 'chatgpt', tokens, last_refresh: new Date().toISOString() };
|
|
37
|
-
fs.writeFileSync(CODEX_AUTH_FILE, JSON.stringify(auth, null, 2), { mode: 0o600 });
|
|
38
|
-
try { fs.chmodSync(CODEX_AUTH_FILE, 0o600); } catch (_) {}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function getCodexOAuthStatus() {
|
|
42
|
-
try {
|
|
43
|
-
if (fs.existsSync(CODEX_AUTH_FILE)) {
|
|
44
|
-
const auth = JSON.parse(fs.readFileSync(CODEX_AUTH_FILE, 'utf8'));
|
|
45
|
-
if (auth.tokens?.access_token || auth.tokens?.refresh_token) {
|
|
46
|
-
const email = parseJwtEmail(auth.tokens?.id_token || '') || '';
|
|
47
|
-
return { hasKey: true, apiKey: email || '****oauth', defaultModel: '', path: CODEX_AUTH_FILE, authMethod: 'oauth' };
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
} catch (_) {}
|
|
51
|
-
return null;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export async function startCodexOAuth(req, { PORT, BASE_URL }) {
|
|
55
|
-
const remote = isRemoteRequest(req);
|
|
56
|
-
const redirectUri = remote
|
|
57
|
-
? `${buildBaseUrl(req, PORT)}${BASE_URL}/codex-oauth2callback`
|
|
58
|
-
: `http://localhost:${CODEX_OAUTH_PORT}/auth/callback`;
|
|
59
|
-
const pkce = generatePkce();
|
|
60
|
-
const csrfToken = crypto.randomBytes(32).toString('hex');
|
|
61
|
-
const relayUrl = remote ? `${buildBaseUrl(req, PORT)}${BASE_URL}/api/codex-oauth/relay` : null;
|
|
62
|
-
const state = encodeOAuthState(csrfToken, relayUrl);
|
|
63
|
-
const params = new URLSearchParams({
|
|
64
|
-
response_type: 'code',
|
|
65
|
-
client_id: CODEX_CLIENT_ID,
|
|
66
|
-
redirect_uri: redirectUri,
|
|
67
|
-
scope: CODEX_SCOPES,
|
|
68
|
-
code_challenge: pkce.codeChallenge,
|
|
69
|
-
code_challenge_method: 'S256',
|
|
70
|
-
id_token_add_organizations: 'true',
|
|
71
|
-
codex_cli_simplified_flow: 'true',
|
|
72
|
-
state,
|
|
73
|
-
});
|
|
74
|
-
const authUrl = `${CODEX_OAUTH_ISSUER}/oauth/authorize?${params.toString()}`;
|
|
75
|
-
const mode = remote ? 'remote' : 'local';
|
|
76
|
-
codexOAuthPending = { pkce, redirectUri, state: csrfToken };
|
|
77
|
-
codexOAuthState = { status: 'pending', error: null, email: null };
|
|
78
|
-
if (codexOAuthPending._timeout) clearTimeout(codexOAuthPending._timeout);
|
|
79
|
-
codexOAuthPending._timeout = setTimeout(() => {
|
|
80
|
-
if (codexOAuthState.status === 'pending') {
|
|
81
|
-
codexOAuthState = { status: 'error', error: 'Authentication timed out', email: null };
|
|
82
|
-
codexOAuthPending = null;
|
|
83
|
-
}
|
|
84
|
-
}, 5 * 60 * 1000);
|
|
85
|
-
return { authUrl, mode };
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export async function exchangeCodexOAuthCode(code, stateParam) {
|
|
89
|
-
if (!codexOAuthPending) throw new Error('No pending OAuth flow. Please start authentication again.');
|
|
90
|
-
if (codexOAuthPending._timeout) { clearTimeout(codexOAuthPending._timeout); codexOAuthPending._timeout = null; }
|
|
91
|
-
const { pkce, redirectUri, state: expectedCsrf } = codexOAuthPending;
|
|
92
|
-
const { csrfToken } = decodeOAuthState(stateParam);
|
|
93
|
-
if (csrfToken !== expectedCsrf) {
|
|
94
|
-
codexOAuthState = { status: 'error', error: 'State mismatch', email: null };
|
|
95
|
-
codexOAuthPending = null;
|
|
96
|
-
throw new Error('State mismatch - possible CSRF attack.');
|
|
97
|
-
}
|
|
98
|
-
if (!code) {
|
|
99
|
-
codexOAuthState = { status: 'error', error: 'No authorization code received', email: null };
|
|
100
|
-
codexOAuthPending = null;
|
|
101
|
-
throw new Error('No authorization code received.');
|
|
102
|
-
}
|
|
103
|
-
const body = new URLSearchParams({
|
|
104
|
-
grant_type: 'authorization_code',
|
|
105
|
-
code,
|
|
106
|
-
redirect_uri: redirectUri,
|
|
107
|
-
client_id: CODEX_CLIENT_ID,
|
|
108
|
-
code_verifier: pkce.codeVerifier,
|
|
109
|
-
});
|
|
110
|
-
const resp = await fetch(`${CODEX_OAUTH_ISSUER}/oauth/token`, {
|
|
111
|
-
method: 'POST',
|
|
112
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
113
|
-
body: body.toString(),
|
|
114
|
-
});
|
|
115
|
-
if (!resp.ok) {
|
|
116
|
-
const text = await resp.text();
|
|
117
|
-
codexOAuthState = { status: 'error', error: `Token exchange failed: ${resp.status}`, email: null };
|
|
118
|
-
codexOAuthPending = null;
|
|
119
|
-
throw new Error(`Token exchange failed (${resp.status}): ${text}`);
|
|
120
|
-
}
|
|
121
|
-
const tokens = await resp.json();
|
|
122
|
-
const email = parseJwtEmail(tokens.id_token || '');
|
|
123
|
-
saveCodexCredentials(tokens);
|
|
124
|
-
codexOAuthState = { status: 'success', error: null, email };
|
|
125
|
-
codexOAuthPending = null;
|
|
126
|
-
return email;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export async function handleCodexOAuthCallback(req, res, PORT) {
|
|
130
|
-
const reqUrl = new URL(req.url, `http://localhost:${PORT}`);
|
|
131
|
-
const code = reqUrl.searchParams.get('code');
|
|
132
|
-
const state = reqUrl.searchParams.get('state');
|
|
133
|
-
const error = reqUrl.searchParams.get('error');
|
|
134
|
-
const errorDesc = reqUrl.searchParams.get('error_description');
|
|
135
|
-
if (error) {
|
|
136
|
-
const desc = errorDesc || error;
|
|
137
|
-
codexOAuthState = { status: 'error', error: desc, email: null };
|
|
138
|
-
codexOAuthPending = null;
|
|
139
|
-
}
|
|
140
|
-
const stateData = decodeOAuthState(state || '');
|
|
141
|
-
if (stateData.relayUrl) {
|
|
142
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
143
|
-
res.end(oauthRelayPage(code, state, errorDesc || error));
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
if (!codexOAuthPending) {
|
|
147
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
148
|
-
res.end(oauthResultPage('Authentication Failed', 'No pending OAuth flow.', false));
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
try {
|
|
152
|
-
if (error) throw new Error(errorDesc || error);
|
|
153
|
-
const email = await exchangeCodexOAuthCode(code, state);
|
|
154
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
155
|
-
res.end(oauthResultPage('Authentication Successful', email ? `Signed in as ${email}` : 'Codex CLI credentials saved.', true));
|
|
156
|
-
} catch (e) {
|
|
157
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
158
|
-
res.end(oauthResultPage('Authentication Failed', e.message, false));
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export function getCodexOAuthState() { return codexOAuthState; }
|
|
163
|
-
export function getCodexOAuthPending() { return codexOAuthPending; }
|
|
164
|
-
export { CODEX_HOME, CODEX_AUTH_FILE };
|