@sanqianx/ai-coding-event-bridge-console 0.7.2 → 0.7.4
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/LICENSE +21 -21
- package/README.md +81 -81
- package/console/index.html +953 -953
- package/package.json +26 -26
- package/src/agent-detect.js +128 -128
- package/src/bridge/bin/commit-boundary.js +64 -64
- package/src/bridge/connectors/capture-guard.js +24 -24
- package/src/bridge/connectors/claude-code/hook-entry.js +45 -45
- package/src/bridge/connectors/claude-code/notify.js +57 -57
- package/src/bridge/connectors/codex/cursor-store.js +83 -83
- package/src/bridge/connectors/codex/hook-entry.js +215 -215
- package/src/bridge/connectors/codex/session-parser.js +100 -100
- package/src/bridge/connectors/opencode/hook-entry.js +55 -55
- package/src/bridge/core/bridge.js +249 -246
- package/src/bridge/core/commit-projection.js +288 -288
- package/src/bridge/core/compaction.js +23 -23
- package/src/bridge/core/consumer-registry.js +111 -111
- package/src/bridge/core/event-schema.js +127 -127
- package/src/bridge/core/fs-utils.js +81 -81
- package/src/bridge/core/journal-router.js +39 -39
- package/src/bridge/core/journal.js +507 -507
- package/src/bridge/core/lock.js +96 -96
- package/src/bridge/core/normalizer.js +114 -114
- package/src/bridge/core/paths.js +12 -12
- package/src/bridge/core/project-registry.js +176 -176
- package/src/bridge/core/repo-context.js +191 -191
- package/src/bridge/core/runtime-home.js +189 -189
- package/src/bridge/core/semver.js +23 -23
- package/src/bridge/core/turn-identity.js +127 -127
- package/src/bridge/index.js +86 -86
- package/src/bridge/installers/claude-code/installer.js +181 -181
- package/src/bridge/installers/codex/installer.js +114 -114
- package/src/bridge/installers/opencode/installer.js +96 -96
- package/src/bridge/installers/zcode/installer.js +200 -200
- package/src/bridge/query/conversation-query.js +194 -194
- package/src/server.js +825 -825
|
@@ -1,194 +1,194 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const path = require('path');
|
|
4
|
-
const crypto = require('crypto');
|
|
5
|
-
const { Journal } = require('../core/journal');
|
|
6
|
-
const { repoIdentityKey } = require('../core/repo-context');
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Headless conversation query over the durable journal. This is the controlled
|
|
10
|
-
* business query surface: it may return full user/assistant bodies, supports
|
|
11
|
-
* cursor pagination for virtualized UIs, and never depends on a host database.
|
|
12
|
-
*/
|
|
13
|
-
class ConversationQuery {
|
|
14
|
-
constructor({ journalDir, journal } = {}) {
|
|
15
|
-
this.journal = journal || new Journal(journalDir);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
async _events() {
|
|
19
|
-
return this.journal.readEvents({});
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
_isConversationEvent(record) {
|
|
23
|
-
return (
|
|
24
|
-
record.schema === 'ai-coding-event/v1'
|
|
25
|
-
&& (record.eventType === 'user_prompt' || record.eventType === 'assistant_response')
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async listProjects() {
|
|
30
|
-
const events = (await this._events()).filter(e => this._isConversationEvent(e) && repoIdentityKey(e.repoIdentity));
|
|
31
|
-
const byProject = new Map();
|
|
32
|
-
for (const event of events) {
|
|
33
|
-
const key = repoIdentityKey(event.repoIdentity);
|
|
34
|
-
const entry = byProject.get(key) || {
|
|
35
|
-
repoIdentity: event.repoIdentity,
|
|
36
|
-
projectPath: event.projectPath || null,
|
|
37
|
-
sources: new Set(),
|
|
38
|
-
eventCount: 0,
|
|
39
|
-
lastSequence: 0,
|
|
40
|
-
lastCapturedAt: null
|
|
41
|
-
};
|
|
42
|
-
entry.sources.add(event.source);
|
|
43
|
-
entry.eventCount += 1;
|
|
44
|
-
if (event.sequence > entry.lastSequence) {
|
|
45
|
-
entry.lastSequence = event.sequence;
|
|
46
|
-
entry.lastCapturedAt = event.capturedAt || null;
|
|
47
|
-
}
|
|
48
|
-
byProject.set(key, entry);
|
|
49
|
-
}
|
|
50
|
-
return [...byProject.values()]
|
|
51
|
-
.map(entry => ({ ...entry, sources: [...entry.sources] }))
|
|
52
|
-
.sort((a, b) => b.lastSequence - a.lastSequence);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
async listSessions({ project } = {}) {
|
|
56
|
-
const projectKey = project === undefined || project === null ? null : repoIdentityKey(project) || project;
|
|
57
|
-
const events = (await this._events()).filter(
|
|
58
|
-
e => this._isConversationEvent(e) && (!projectKey || repoIdentityKey(e.repoIdentity) === projectKey) && e.sessionId
|
|
59
|
-
);
|
|
60
|
-
const bySession = new Map();
|
|
61
|
-
for (const event of events) {
|
|
62
|
-
const entry = bySession.get(event.sessionId) || {
|
|
63
|
-
sessionId: event.sessionId,
|
|
64
|
-
repoIdentity: event.repoIdentity,
|
|
65
|
-
source: event.source,
|
|
66
|
-
eventCount: 0,
|
|
67
|
-
firstSequence: event.sequence,
|
|
68
|
-
lastSequence: event.sequence
|
|
69
|
-
};
|
|
70
|
-
entry.eventCount += 1;
|
|
71
|
-
entry.lastSequence = Math.max(entry.lastSequence, event.sequence);
|
|
72
|
-
entry.firstSequence = Math.min(entry.firstSequence, event.sequence);
|
|
73
|
-
bySession.set(event.sessionId, entry);
|
|
74
|
-
}
|
|
75
|
-
return [...bySession.values()].sort((a, b) => b.lastSequence - a.lastSequence);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
_localDay(iso) {
|
|
79
|
-
const d = new Date(iso);
|
|
80
|
-
if (Number.isNaN(d.getTime())) return null;
|
|
81
|
-
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
_projectTurns(events) {
|
|
85
|
-
// Turn assembly by turnId; assistant events without turnId bind to the
|
|
86
|
-
// single open turn of their session (same rule as the journal projection).
|
|
87
|
-
const turns = [];
|
|
88
|
-
const open = new Map();
|
|
89
|
-
for (const event of events) {
|
|
90
|
-
if (event.eventType === 'user_prompt' && event.turnId) {
|
|
91
|
-
const turn = {
|
|
92
|
-
turnId: event.turnId,
|
|
93
|
-
sessionId: event.sessionId || null,
|
|
94
|
-
source: event.source,
|
|
95
|
-
repoIdentity: event.repoIdentity,
|
|
96
|
-
day: this._localDay(event.capturedAt),
|
|
97
|
-
startSequence: event.sequence,
|
|
98
|
-
endSequence: event.sequence,
|
|
99
|
-
userEvents: [],
|
|
100
|
-
assistantEvents: []
|
|
101
|
-
};
|
|
102
|
-
turn.userEvents.push(this._projectEvent(event));
|
|
103
|
-
turns.push(turn);
|
|
104
|
-
open.set(event.turnId, turn);
|
|
105
|
-
} else if (event.eventType === 'assistant_response') {
|
|
106
|
-
let turn = event.turnId ? open.get(event.turnId) : null;
|
|
107
|
-
if (!turn && event.sessionId) {
|
|
108
|
-
const openForSession = [...open.values()].filter(t => t.sessionId === event.sessionId);
|
|
109
|
-
if (openForSession.length === 1) turn = openForSession[0];
|
|
110
|
-
}
|
|
111
|
-
if (turn) {
|
|
112
|
-
turn.assistantEvents.push(this._projectEvent(event));
|
|
113
|
-
turn.endSequence = event.sequence;
|
|
114
|
-
open.delete(turn.turnId);
|
|
115
|
-
}
|
|
116
|
-
// Orphan assistant evidence is intentionally not surfaced as a turn.
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
return turns;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
_projectEvent(event) {
|
|
123
|
-
// Controlled projection: full conversation bodies, journal internals stripped.
|
|
124
|
-
return {
|
|
125
|
-
sequence: event.sequence,
|
|
126
|
-
eventId: event.eventId,
|
|
127
|
-
role: event.role,
|
|
128
|
-
content: event.content === undefined ? null : event.content,
|
|
129
|
-
source: event.source,
|
|
130
|
-
sessionId: event.sessionId || null,
|
|
131
|
-
turnId: event.turnId || null,
|
|
132
|
-
capturedAt: event.capturedAt || null,
|
|
133
|
-
identityConfidence: event.identityConfidence || 'unavailable',
|
|
134
|
-
captureStatus: event.captureStatus || 'complete'
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
_encodeCursor(turns, lastIndex) {
|
|
139
|
-
return Buffer.from(
|
|
140
|
-
JSON.stringify({ v: 1, last: turns[lastIndex] ? turns[lastIndex].startSequence : 0, n: lastIndex + 1 })
|
|
141
|
-
).toString('base64url');
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
async turns({ project, date, cursor, limit = 50 } = {}) {
|
|
145
|
-
const projectKey = project === undefined || project === null ? null : repoIdentityKey(project) || project;
|
|
146
|
-
const all = (await this._events()).filter(
|
|
147
|
-
e => this._isConversationEvent(e) && (!projectKey || repoIdentityKey(e.repoIdentity) === projectKey)
|
|
148
|
-
);
|
|
149
|
-
let turns = this._projectTurns(all);
|
|
150
|
-
if (date) turns = turns.filter(t => t.day === date);
|
|
151
|
-
turns.sort((a, b) => a.startSequence - b.startSequence);
|
|
152
|
-
let start = 0;
|
|
153
|
-
if (cursor) {
|
|
154
|
-
let decoded;
|
|
155
|
-
try {
|
|
156
|
-
decoded = JSON.parse(Buffer.from(String(cursor), 'base64url').toString('utf8'));
|
|
157
|
-
} catch (_) {
|
|
158
|
-
decoded = null;
|
|
159
|
-
}
|
|
160
|
-
if (!decoded || decoded.v !== 1 || typeof decoded.last !== 'number') {
|
|
161
|
-
const err = new Error('conversation cursor is invalid; restart the query');
|
|
162
|
-
err.code = 'LOG_CURSOR_EXPIRED';
|
|
163
|
-
throw err;
|
|
164
|
-
}
|
|
165
|
-
start = turns.findIndex(t => t.startSequence > decoded.last);
|
|
166
|
-
if (start === -1) start = turns.length;
|
|
167
|
-
}
|
|
168
|
-
const pageSize = Math.max(1, Math.min(Number(limit) || 50, 200));
|
|
169
|
-
const page = turns.slice(start, start + pageSize);
|
|
170
|
-
return {
|
|
171
|
-
project: project || null,
|
|
172
|
-
date: date || null,
|
|
173
|
-
turns: page,
|
|
174
|
-
nextCursor: start + pageSize < turns.length ? this._encodeCursor(turns, start + pageSize - 1) : null,
|
|
175
|
-
totalTurns: turns.length
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
async searchConversations({ project, q, limit = 20 } = {}) {
|
|
180
|
-
const needle = String(q || '').toLowerCase();
|
|
181
|
-
const projectKey = project === undefined || project === null ? null : repoIdentityKey(project) || project;
|
|
182
|
-
const events = (await this._events()).filter(
|
|
183
|
-
e => this._isConversationEvent(e) && (!projectKey || repoIdentityKey(e.repoIdentity) === projectKey) && typeof e.content === 'string' && e.content.toLowerCase().includes(needle)
|
|
184
|
-
);
|
|
185
|
-
return {
|
|
186
|
-
matches: events
|
|
187
|
-
.slice(-Math.max(1, Math.min(Number(limit) || 20, 100)))
|
|
188
|
-
.reverse()
|
|
189
|
-
.map(e => this._projectEvent(e))
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
module.exports = { ConversationQuery };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
const { Journal } = require('../core/journal');
|
|
6
|
+
const { repoIdentityKey } = require('../core/repo-context');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Headless conversation query over the durable journal. This is the controlled
|
|
10
|
+
* business query surface: it may return full user/assistant bodies, supports
|
|
11
|
+
* cursor pagination for virtualized UIs, and never depends on a host database.
|
|
12
|
+
*/
|
|
13
|
+
class ConversationQuery {
|
|
14
|
+
constructor({ journalDir, journal } = {}) {
|
|
15
|
+
this.journal = journal || new Journal(journalDir);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async _events() {
|
|
19
|
+
return this.journal.readEvents({});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
_isConversationEvent(record) {
|
|
23
|
+
return (
|
|
24
|
+
record.schema === 'ai-coding-event/v1'
|
|
25
|
+
&& (record.eventType === 'user_prompt' || record.eventType === 'assistant_response')
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async listProjects() {
|
|
30
|
+
const events = (await this._events()).filter(e => this._isConversationEvent(e) && repoIdentityKey(e.repoIdentity));
|
|
31
|
+
const byProject = new Map();
|
|
32
|
+
for (const event of events) {
|
|
33
|
+
const key = repoIdentityKey(event.repoIdentity);
|
|
34
|
+
const entry = byProject.get(key) || {
|
|
35
|
+
repoIdentity: event.repoIdentity,
|
|
36
|
+
projectPath: event.projectPath || null,
|
|
37
|
+
sources: new Set(),
|
|
38
|
+
eventCount: 0,
|
|
39
|
+
lastSequence: 0,
|
|
40
|
+
lastCapturedAt: null
|
|
41
|
+
};
|
|
42
|
+
entry.sources.add(event.source);
|
|
43
|
+
entry.eventCount += 1;
|
|
44
|
+
if (event.sequence > entry.lastSequence) {
|
|
45
|
+
entry.lastSequence = event.sequence;
|
|
46
|
+
entry.lastCapturedAt = event.capturedAt || null;
|
|
47
|
+
}
|
|
48
|
+
byProject.set(key, entry);
|
|
49
|
+
}
|
|
50
|
+
return [...byProject.values()]
|
|
51
|
+
.map(entry => ({ ...entry, sources: [...entry.sources] }))
|
|
52
|
+
.sort((a, b) => b.lastSequence - a.lastSequence);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async listSessions({ project } = {}) {
|
|
56
|
+
const projectKey = project === undefined || project === null ? null : repoIdentityKey(project) || project;
|
|
57
|
+
const events = (await this._events()).filter(
|
|
58
|
+
e => this._isConversationEvent(e) && (!projectKey || repoIdentityKey(e.repoIdentity) === projectKey) && e.sessionId
|
|
59
|
+
);
|
|
60
|
+
const bySession = new Map();
|
|
61
|
+
for (const event of events) {
|
|
62
|
+
const entry = bySession.get(event.sessionId) || {
|
|
63
|
+
sessionId: event.sessionId,
|
|
64
|
+
repoIdentity: event.repoIdentity,
|
|
65
|
+
source: event.source,
|
|
66
|
+
eventCount: 0,
|
|
67
|
+
firstSequence: event.sequence,
|
|
68
|
+
lastSequence: event.sequence
|
|
69
|
+
};
|
|
70
|
+
entry.eventCount += 1;
|
|
71
|
+
entry.lastSequence = Math.max(entry.lastSequence, event.sequence);
|
|
72
|
+
entry.firstSequence = Math.min(entry.firstSequence, event.sequence);
|
|
73
|
+
bySession.set(event.sessionId, entry);
|
|
74
|
+
}
|
|
75
|
+
return [...bySession.values()].sort((a, b) => b.lastSequence - a.lastSequence);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
_localDay(iso) {
|
|
79
|
+
const d = new Date(iso);
|
|
80
|
+
if (Number.isNaN(d.getTime())) return null;
|
|
81
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
_projectTurns(events) {
|
|
85
|
+
// Turn assembly by turnId; assistant events without turnId bind to the
|
|
86
|
+
// single open turn of their session (same rule as the journal projection).
|
|
87
|
+
const turns = [];
|
|
88
|
+
const open = new Map();
|
|
89
|
+
for (const event of events) {
|
|
90
|
+
if (event.eventType === 'user_prompt' && event.turnId) {
|
|
91
|
+
const turn = {
|
|
92
|
+
turnId: event.turnId,
|
|
93
|
+
sessionId: event.sessionId || null,
|
|
94
|
+
source: event.source,
|
|
95
|
+
repoIdentity: event.repoIdentity,
|
|
96
|
+
day: this._localDay(event.capturedAt),
|
|
97
|
+
startSequence: event.sequence,
|
|
98
|
+
endSequence: event.sequence,
|
|
99
|
+
userEvents: [],
|
|
100
|
+
assistantEvents: []
|
|
101
|
+
};
|
|
102
|
+
turn.userEvents.push(this._projectEvent(event));
|
|
103
|
+
turns.push(turn);
|
|
104
|
+
open.set(event.turnId, turn);
|
|
105
|
+
} else if (event.eventType === 'assistant_response') {
|
|
106
|
+
let turn = event.turnId ? open.get(event.turnId) : null;
|
|
107
|
+
if (!turn && event.sessionId) {
|
|
108
|
+
const openForSession = [...open.values()].filter(t => t.sessionId === event.sessionId);
|
|
109
|
+
if (openForSession.length === 1) turn = openForSession[0];
|
|
110
|
+
}
|
|
111
|
+
if (turn) {
|
|
112
|
+
turn.assistantEvents.push(this._projectEvent(event));
|
|
113
|
+
turn.endSequence = event.sequence;
|
|
114
|
+
open.delete(turn.turnId);
|
|
115
|
+
}
|
|
116
|
+
// Orphan assistant evidence is intentionally not surfaced as a turn.
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return turns;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
_projectEvent(event) {
|
|
123
|
+
// Controlled projection: full conversation bodies, journal internals stripped.
|
|
124
|
+
return {
|
|
125
|
+
sequence: event.sequence,
|
|
126
|
+
eventId: event.eventId,
|
|
127
|
+
role: event.role,
|
|
128
|
+
content: event.content === undefined ? null : event.content,
|
|
129
|
+
source: event.source,
|
|
130
|
+
sessionId: event.sessionId || null,
|
|
131
|
+
turnId: event.turnId || null,
|
|
132
|
+
capturedAt: event.capturedAt || null,
|
|
133
|
+
identityConfidence: event.identityConfidence || 'unavailable',
|
|
134
|
+
captureStatus: event.captureStatus || 'complete'
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
_encodeCursor(turns, lastIndex) {
|
|
139
|
+
return Buffer.from(
|
|
140
|
+
JSON.stringify({ v: 1, last: turns[lastIndex] ? turns[lastIndex].startSequence : 0, n: lastIndex + 1 })
|
|
141
|
+
).toString('base64url');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async turns({ project, date, cursor, limit = 50 } = {}) {
|
|
145
|
+
const projectKey = project === undefined || project === null ? null : repoIdentityKey(project) || project;
|
|
146
|
+
const all = (await this._events()).filter(
|
|
147
|
+
e => this._isConversationEvent(e) && (!projectKey || repoIdentityKey(e.repoIdentity) === projectKey)
|
|
148
|
+
);
|
|
149
|
+
let turns = this._projectTurns(all);
|
|
150
|
+
if (date) turns = turns.filter(t => t.day === date);
|
|
151
|
+
turns.sort((a, b) => a.startSequence - b.startSequence);
|
|
152
|
+
let start = 0;
|
|
153
|
+
if (cursor) {
|
|
154
|
+
let decoded;
|
|
155
|
+
try {
|
|
156
|
+
decoded = JSON.parse(Buffer.from(String(cursor), 'base64url').toString('utf8'));
|
|
157
|
+
} catch (_) {
|
|
158
|
+
decoded = null;
|
|
159
|
+
}
|
|
160
|
+
if (!decoded || decoded.v !== 1 || typeof decoded.last !== 'number') {
|
|
161
|
+
const err = new Error('conversation cursor is invalid; restart the query');
|
|
162
|
+
err.code = 'LOG_CURSOR_EXPIRED';
|
|
163
|
+
throw err;
|
|
164
|
+
}
|
|
165
|
+
start = turns.findIndex(t => t.startSequence > decoded.last);
|
|
166
|
+
if (start === -1) start = turns.length;
|
|
167
|
+
}
|
|
168
|
+
const pageSize = Math.max(1, Math.min(Number(limit) || 50, 200));
|
|
169
|
+
const page = turns.slice(start, start + pageSize);
|
|
170
|
+
return {
|
|
171
|
+
project: project || null,
|
|
172
|
+
date: date || null,
|
|
173
|
+
turns: page,
|
|
174
|
+
nextCursor: start + pageSize < turns.length ? this._encodeCursor(turns, start + pageSize - 1) : null,
|
|
175
|
+
totalTurns: turns.length
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async searchConversations({ project, q, limit = 20 } = {}) {
|
|
180
|
+
const needle = String(q || '').toLowerCase();
|
|
181
|
+
const projectKey = project === undefined || project === null ? null : repoIdentityKey(project) || project;
|
|
182
|
+
const events = (await this._events()).filter(
|
|
183
|
+
e => this._isConversationEvent(e) && (!projectKey || repoIdentityKey(e.repoIdentity) === projectKey) && typeof e.content === 'string' && e.content.toLowerCase().includes(needle)
|
|
184
|
+
);
|
|
185
|
+
return {
|
|
186
|
+
matches: events
|
|
187
|
+
.slice(-Math.max(1, Math.min(Number(limit) || 20, 100)))
|
|
188
|
+
.reverse()
|
|
189
|
+
.map(e => this._projectEvent(e))
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
module.exports = { ConversationQuery };
|