remcodex 0.1.0-beta.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.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +331 -0
  3. package/dist/server/src/app.js +186 -0
  4. package/dist/server/src/cli.js +270 -0
  5. package/dist/server/src/controllers/codex-options.controller.js +199 -0
  6. package/dist/server/src/controllers/message.controller.js +21 -0
  7. package/dist/server/src/controllers/project.controller.js +44 -0
  8. package/dist/server/src/controllers/session.controller.js +175 -0
  9. package/dist/server/src/db/client.js +10 -0
  10. package/dist/server/src/db/migrations.js +32 -0
  11. package/dist/server/src/gateways/ws.gateway.js +60 -0
  12. package/dist/server/src/services/codex-app-server-runner.js +363 -0
  13. package/dist/server/src/services/codex-exec-runner.js +147 -0
  14. package/dist/server/src/services/codex-rollout-sync.js +977 -0
  15. package/dist/server/src/services/codex-runner.js +11 -0
  16. package/dist/server/src/services/codex-stream-events.js +478 -0
  17. package/dist/server/src/services/event-store.js +328 -0
  18. package/dist/server/src/services/project-manager.js +130 -0
  19. package/dist/server/src/services/pty-runner.js +72 -0
  20. package/dist/server/src/services/session-manager.js +1586 -0
  21. package/dist/server/src/services/session-timeline-service.js +181 -0
  22. package/dist/server/src/types/codex-launch.js +2 -0
  23. package/dist/server/src/types/models.js +37 -0
  24. package/dist/server/src/utils/ansi.js +143 -0
  25. package/dist/server/src/utils/codex-launch.js +102 -0
  26. package/dist/server/src/utils/codex-quota.js +179 -0
  27. package/dist/server/src/utils/codex-status.js +163 -0
  28. package/dist/server/src/utils/codex-ui-options.js +114 -0
  29. package/dist/server/src/utils/command.js +46 -0
  30. package/dist/server/src/utils/errors.js +16 -0
  31. package/dist/server/src/utils/ids.js +7 -0
  32. package/dist/server/src/utils/node-pty.js +29 -0
  33. package/package.json +36 -0
  34. package/scripts/fix-node-pty-helper.js +36 -0
  35. package/web/api.js +175 -0
  36. package/web/app.js +8082 -0
  37. package/web/components/composer.js +627 -0
  38. package/web/components/session-workbench.js +173 -0
  39. package/web/i18n/index.js +171 -0
  40. package/web/i18n/locales/de.js +50 -0
  41. package/web/i18n/locales/en.js +320 -0
  42. package/web/i18n/locales/es.js +50 -0
  43. package/web/i18n/locales/fr.js +50 -0
  44. package/web/i18n/locales/ja.js +50 -0
  45. package/web/i18n/locales/ko.js +50 -0
  46. package/web/i18n/locales/pt-BR.js +50 -0
  47. package/web/i18n/locales/ru.js +50 -0
  48. package/web/i18n/locales/zh-CN.js +320 -0
  49. package/web/i18n/locales/zh-Hant.js +53 -0
  50. package/web/index.html +23 -0
  51. package/web/message-rich-text.js +218 -0
  52. package/web/session-command-activity.js +980 -0
  53. package/web/session-event-adapter.js +826 -0
  54. package/web/session-timeline-reducer.js +728 -0
  55. package/web/session-timeline-renderer.js +656 -0
  56. package/web/session-ws.js +31 -0
  57. package/web/styles.css +5665 -0
  58. package/web/vendor/markdown-it.js +6969 -0
@@ -0,0 +1,328 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EventStore = void 0;
4
+ const node_events_1 = require("node:events");
5
+ const ids_1 = require("../utils/ids");
6
+ class EventStore {
7
+ db;
8
+ emitter = new node_events_1.EventEmitter();
9
+ latestQuotaCache = new Map();
10
+ constructor(db) {
11
+ this.db = db;
12
+ }
13
+ append(sessionId, input) {
14
+ const seq = this.nextSeq(sessionId);
15
+ const id = input.id?.trim() || (0, ids_1.createId)("evt");
16
+ const timestamp = input.timestamp?.trim() || new Date().toISOString();
17
+ const payloadJson = JSON.stringify(input.payload ?? {});
18
+ const stream = this.normalizeStream(input.stream);
19
+ this.db
20
+ .prepare(`
21
+ INSERT INTO session_events (
22
+ id,
23
+ session_id,
24
+ turn_id,
25
+ seq,
26
+ event_type,
27
+ message_id,
28
+ call_id,
29
+ request_id,
30
+ phase,
31
+ stream,
32
+ payload_json,
33
+ created_at
34
+ )
35
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
36
+ `)
37
+ .run(id, sessionId, input.turnId, seq, input.type, input.messageId, input.callId, input.requestId, input.phase, stream, payloadJson, timestamp);
38
+ const row = this.db
39
+ .prepare(`
40
+ SELECT
41
+ id,
42
+ session_id,
43
+ turn_id,
44
+ seq,
45
+ event_type,
46
+ message_id,
47
+ call_id,
48
+ request_id,
49
+ phase,
50
+ stream,
51
+ payload_json,
52
+ created_at
53
+ FROM session_events
54
+ WHERE id = ?
55
+ `)
56
+ .get(id);
57
+ const event = this.toPayload(row);
58
+ this.captureLatestQuota(sessionId, event);
59
+ this.emitter.emit(this.channel(sessionId), event);
60
+ return event;
61
+ }
62
+ list(sessionId, options = {}) {
63
+ const safeLimit = Math.max(1, Math.min(options.limit ?? 200, 200));
64
+ const after = Math.max(0, options.after ?? 0);
65
+ const before = Math.max(0, options.before ?? 0);
66
+ if (before > 0) {
67
+ const rows = this.db
68
+ .prepare(`
69
+ SELECT
70
+ id,
71
+ session_id,
72
+ turn_id,
73
+ seq,
74
+ event_type,
75
+ message_id,
76
+ call_id,
77
+ request_id,
78
+ phase,
79
+ stream,
80
+ payload_json,
81
+ created_at
82
+ FROM session_events
83
+ WHERE session_id = ? AND seq < ?
84
+ ORDER BY seq DESC
85
+ LIMIT ?
86
+ `)
87
+ .all(sessionId, before, safeLimit + 1);
88
+ const hasMoreBefore = rows.length > safeLimit;
89
+ const pageRows = rows.slice(0, safeLimit).reverse();
90
+ return {
91
+ items: pageRows.map((row) => this.toPayload(row)),
92
+ nextCursor: pageRows.length > 0 ? pageRows[pageRows.length - 1].seq : after,
93
+ beforeCursor: pageRows.length > 0 ? pageRows[0].seq : before,
94
+ hasMoreBefore,
95
+ };
96
+ }
97
+ if (after > 0) {
98
+ const rows = this.db
99
+ .prepare(`
100
+ SELECT
101
+ id,
102
+ session_id,
103
+ turn_id,
104
+ seq,
105
+ event_type,
106
+ message_id,
107
+ call_id,
108
+ request_id,
109
+ phase,
110
+ stream,
111
+ payload_json,
112
+ created_at
113
+ FROM session_events
114
+ WHERE session_id = ? AND seq > ?
115
+ ORDER BY seq ASC
116
+ LIMIT ?
117
+ `)
118
+ .all(sessionId, after, safeLimit);
119
+ return {
120
+ items: rows.map((row) => this.toPayload(row)),
121
+ nextCursor: rows.length > 0 ? rows[rows.length - 1].seq : after,
122
+ beforeCursor: rows.length > 0 ? rows[0].seq : 0,
123
+ hasMoreBefore: rows.length > 0 ? rows[0].seq > 1 : false,
124
+ };
125
+ }
126
+ const rows = this.db
127
+ .prepare(`
128
+ SELECT
129
+ id,
130
+ session_id,
131
+ turn_id,
132
+ seq,
133
+ event_type,
134
+ message_id,
135
+ call_id,
136
+ request_id,
137
+ phase,
138
+ stream,
139
+ payload_json,
140
+ created_at
141
+ FROM session_events
142
+ WHERE session_id = ?
143
+ ORDER BY seq DESC
144
+ LIMIT ?
145
+ `)
146
+ .all(sessionId, safeLimit + 1);
147
+ const hasMoreBefore = rows.length > safeLimit;
148
+ const pageRows = rows.slice(0, safeLimit).reverse();
149
+ return {
150
+ items: pageRows.map((row) => this.toPayload(row)),
151
+ nextCursor: pageRows.length > 0 ? pageRows[pageRows.length - 1].seq : after,
152
+ beforeCursor: pageRows.length > 0 ? pageRows[0].seq : 0,
153
+ hasMoreBefore,
154
+ };
155
+ }
156
+ listAll(sessionId) {
157
+ const rows = this.db
158
+ .prepare(`
159
+ SELECT
160
+ id,
161
+ session_id,
162
+ turn_id,
163
+ seq,
164
+ event_type,
165
+ message_id,
166
+ call_id,
167
+ request_id,
168
+ phase,
169
+ stream,
170
+ payload_json,
171
+ created_at
172
+ FROM session_events
173
+ WHERE session_id = ?
174
+ ORDER BY seq ASC
175
+ `)
176
+ .all(sessionId);
177
+ return rows.map((row) => this.toPayload(row));
178
+ }
179
+ latestQuota(sessionId) {
180
+ const cached = this.latestQuotaCache.get(sessionId);
181
+ if (cached) {
182
+ return cached;
183
+ }
184
+ const rows = this.db
185
+ .prepare(`
186
+ SELECT payload_json
187
+ FROM session_events
188
+ WHERE session_id = ?
189
+ AND event_type = 'token_count'
190
+ ORDER BY seq DESC
191
+ `)
192
+ .all(sessionId);
193
+ for (const row of rows) {
194
+ const payload = this.tryParse(row.payload_json ?? null);
195
+ if (payload && this.hasUsableQuota(payload)) {
196
+ this.latestQuotaCache.set(sessionId, payload);
197
+ return payload;
198
+ }
199
+ }
200
+ return null;
201
+ }
202
+ latestPendingApproval(sessionId) {
203
+ const rows = this.db
204
+ .prepare(`
205
+ SELECT event_type, request_id, payload_json, seq
206
+ FROM session_events
207
+ WHERE session_id = ?
208
+ AND event_type IN ('approval.requested', 'approval.resolved')
209
+ ORDER BY seq ASC
210
+ `)
211
+ .all(sessionId);
212
+ if (rows.length === 0) {
213
+ return null;
214
+ }
215
+ const pending = new Map();
216
+ for (const row of rows) {
217
+ if (row.event_type === "approval.requested") {
218
+ const payload = this.tryParse(row.payload_json);
219
+ const requestId = row.request_id || payload?.requestId || "";
220
+ if (requestId) {
221
+ pending.set(requestId, {
222
+ ...payload,
223
+ requestId,
224
+ });
225
+ }
226
+ continue;
227
+ }
228
+ const payload = this.tryParse(row.payload_json);
229
+ const requestId = row.request_id || payload?.requestId || "";
230
+ if (requestId) {
231
+ pending.delete(requestId);
232
+ }
233
+ }
234
+ const unresolved = [...pending.values()].sort((a, b) => String(a.createdAt || "").localeCompare(String(b.createdAt || "")));
235
+ return unresolved[0] ?? null;
236
+ }
237
+ subscribe(sessionId, listener) {
238
+ const channel = this.channel(sessionId);
239
+ this.emitter.on(channel, listener);
240
+ return () => {
241
+ this.emitter.off(channel, listener);
242
+ };
243
+ }
244
+ nextSeq(sessionId) {
245
+ const row = this.db
246
+ .prepare(`
247
+ SELECT COALESCE(MAX(seq), 0) AS current_seq
248
+ FROM session_events
249
+ WHERE session_id = ?
250
+ `)
251
+ .get(sessionId);
252
+ return row.current_seq + 1;
253
+ }
254
+ toPayload(row) {
255
+ return {
256
+ id: row.id,
257
+ sessionId: row.session_id,
258
+ type: row.event_type,
259
+ seq: row.seq,
260
+ timestamp: row.created_at,
261
+ turnId: row.turn_id,
262
+ messageId: row.message_id,
263
+ callId: row.call_id,
264
+ requestId: row.request_id,
265
+ phase: row.phase,
266
+ stream: this.normalizeStream(row.stream),
267
+ payload: this.tryParse(row.payload_json) ?? {},
268
+ };
269
+ }
270
+ tryParse(raw) {
271
+ if (!raw) {
272
+ return null;
273
+ }
274
+ try {
275
+ return JSON.parse(raw);
276
+ }
277
+ catch {
278
+ return null;
279
+ }
280
+ }
281
+ normalizeStream(stream) {
282
+ switch (stream) {
283
+ case "stdout":
284
+ case "stderr":
285
+ return stream;
286
+ default:
287
+ return null;
288
+ }
289
+ }
290
+ channel(sessionId) {
291
+ return `session:${sessionId}`;
292
+ }
293
+ captureLatestQuota(sessionId, event) {
294
+ if (event.type !== "token_count") {
295
+ return;
296
+ }
297
+ const payload = event.payload;
298
+ if (payload && this.hasUsableQuota(payload)) {
299
+ this.latestQuotaCache.set(sessionId, payload);
300
+ }
301
+ }
302
+ hasUsableQuota(payload) {
303
+ const rateLimits = payload.rateLimits && typeof payload.rateLimits === "object"
304
+ ? payload.rateLimits
305
+ : {};
306
+ const primary = rateLimits.primary && typeof rateLimits.primary === "object"
307
+ ? rateLimits.primary
308
+ : {};
309
+ const secondary = rateLimits.secondary && typeof rateLimits.secondary === "object"
310
+ ? rateLimits.secondary
311
+ : {};
312
+ return (this.readQuotaField(primary.used_percent) != null ||
313
+ this.readQuotaField(primary.resets_at) != null ||
314
+ this.readQuotaField(secondary.used_percent) != null ||
315
+ this.readQuotaField(secondary.resets_at) != null);
316
+ }
317
+ readQuotaField(input) {
318
+ if (typeof input === "number" && Number.isFinite(input)) {
319
+ return input;
320
+ }
321
+ if (typeof input === "string" && input.trim()) {
322
+ const parsed = Number.parseFloat(input);
323
+ return Number.isFinite(parsed) ? parsed : null;
324
+ }
325
+ return null;
326
+ }
327
+ }
328
+ exports.EventStore = EventStore;
@@ -0,0 +1,130 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ProjectManager = void 0;
7
+ const node_fs_1 = require("node:fs");
8
+ const node_os_1 = require("node:os");
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const errors_1 = require("../utils/errors");
11
+ const ids_1 = require("../utils/ids");
12
+ class ProjectManager {
13
+ db;
14
+ allowedRoots;
15
+ constructor(db, projectRootsEnv, repoRoot) {
16
+ this.db = db;
17
+ this.allowedRoots = (projectRootsEnv ?? (0, node_os_1.homedir)())
18
+ .split(",")
19
+ .map((item) => item.trim())
20
+ .filter(Boolean)
21
+ .map((item) => node_path_1.default.resolve(item));
22
+ }
23
+ listProjects() {
24
+ return this.db
25
+ .prepare(`
26
+ SELECT id, name, path, created_at
27
+ FROM projects
28
+ ORDER BY created_at DESC
29
+ `)
30
+ .all();
31
+ }
32
+ getProject(projectId) {
33
+ return (this.db
34
+ .prepare(`
35
+ SELECT id, name, path, created_at
36
+ FROM projects
37
+ WHERE id = ?
38
+ `)
39
+ .get(projectId) ?? null);
40
+ }
41
+ createProject(input) {
42
+ const name = input.name.trim();
43
+ const resolvedPath = node_path_1.default.resolve(input.path.trim());
44
+ if (!name) {
45
+ throw new errors_1.AppError(400, "Project name is required.");
46
+ }
47
+ if (!input.path.trim()) {
48
+ throw new errors_1.AppError(400, "Project path is required.");
49
+ }
50
+ if (!(0, node_fs_1.existsSync)(resolvedPath) && input.createMissing) {
51
+ (0, node_fs_1.mkdirSync)(resolvedPath, { recursive: true });
52
+ }
53
+ if (!(0, node_fs_1.existsSync)(resolvedPath) || !(0, node_fs_1.statSync)(resolvedPath).isDirectory()) {
54
+ throw new errors_1.AppError(400, "Project path does not exist or is not a directory.");
55
+ }
56
+ if (!this.isAllowed(resolvedPath)) {
57
+ throw new errors_1.AppError(400, `Project path is outside allowed roots: ${this.allowedRoots.join(", ")}`);
58
+ }
59
+ const duplicated = this.db
60
+ .prepare(`
61
+ SELECT id
62
+ FROM projects
63
+ WHERE path = ?
64
+ `)
65
+ .get(resolvedPath);
66
+ if (duplicated) {
67
+ throw new errors_1.AppError(409, "Project path is already registered.");
68
+ }
69
+ const project = {
70
+ id: (0, ids_1.createId)("proj"),
71
+ name,
72
+ path: resolvedPath,
73
+ created_at: new Date().toISOString(),
74
+ };
75
+ this.db
76
+ .prepare(`
77
+ INSERT INTO projects (id, name, path, created_at)
78
+ VALUES (?, ?, ?, ?)
79
+ `)
80
+ .run(project.id, project.name, project.path, project.created_at);
81
+ return project;
82
+ }
83
+ listAllowedRoots() {
84
+ return [...this.allowedRoots];
85
+ }
86
+ browseDirectories(targetPath) {
87
+ const rawTarget = String(targetPath || "").trim();
88
+ if (!rawTarget) {
89
+ return {
90
+ currentPath: null,
91
+ parentPath: null,
92
+ items: this.allowedRoots.map((root) => ({
93
+ name: node_path_1.default.basename(root) || root,
94
+ path: root,
95
+ })),
96
+ };
97
+ }
98
+ const resolvedPath = node_path_1.default.resolve(rawTarget);
99
+ const root = this.findAllowedRoot(resolvedPath);
100
+ if (!root) {
101
+ throw new errors_1.AppError(400, `Project path is outside allowed roots: ${this.allowedRoots.join(", ")}`);
102
+ }
103
+ if (!(0, node_fs_1.existsSync)(resolvedPath) || !(0, node_fs_1.statSync)(resolvedPath).isDirectory()) {
104
+ throw new errors_1.AppError(400, "Project path does not exist or is not a directory.");
105
+ }
106
+ const items = (0, node_fs_1.readdirSync)(resolvedPath, { withFileTypes: true })
107
+ .filter((entry) => entry.isDirectory())
108
+ .map((entry) => ({
109
+ name: entry.name,
110
+ path: node_path_1.default.join(resolvedPath, entry.name),
111
+ }))
112
+ .sort((left, right) => left.name.localeCompare(right.name, undefined, { sensitivity: "base" }));
113
+ const parentPath = resolvedPath === root ? null : node_path_1.default.dirname(resolvedPath);
114
+ return {
115
+ currentPath: resolvedPath,
116
+ parentPath,
117
+ items,
118
+ };
119
+ }
120
+ isAllowed(targetPath) {
121
+ return Boolean(this.findAllowedRoot(targetPath));
122
+ }
123
+ findAllowedRoot(targetPath) {
124
+ return (this.allowedRoots.find((root) => {
125
+ const relative = node_path_1.default.relative(root, targetPath);
126
+ return relative === "" || (!relative.startsWith("..") && !node_path_1.default.isAbsolute(relative));
127
+ }) ?? null);
128
+ }
129
+ }
130
+ exports.ProjectManager = ProjectManager;
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PtyRunner = void 0;
4
+ const node_pty_1 = require("node-pty");
5
+ const node_pty_2 = require("../utils/node-pty");
6
+ class PtyRunner {
7
+ command;
8
+ cwd;
9
+ process = null;
10
+ dataListeners = new Set();
11
+ exitListeners = new Set();
12
+ stopTimer = null;
13
+ constructor(command, cwd) {
14
+ this.command = command;
15
+ this.cwd = cwd;
16
+ }
17
+ start() {
18
+ (0, node_pty_2.ensureNodePtyHelperExecutable)();
19
+ const shell = process.env.SHELL ?? "/bin/zsh";
20
+ this.process = (0, node_pty_1.spawn)(shell, ["-lc", this.command], {
21
+ cwd: this.cwd,
22
+ cols: 120,
23
+ rows: 30,
24
+ name: "xterm-256color",
25
+ env: {
26
+ ...process.env,
27
+ TERM: "xterm-256color",
28
+ },
29
+ });
30
+ this.process.onData((chunk) => {
31
+ this.dataListeners.forEach((listener) => listener(chunk));
32
+ });
33
+ this.process.onExit(({ exitCode }) => {
34
+ if (this.stopTimer) {
35
+ clearTimeout(this.stopTimer);
36
+ this.stopTimer = null;
37
+ }
38
+ this.process = null;
39
+ this.exitListeners.forEach((listener) => listener(exitCode));
40
+ });
41
+ return this.process.pid;
42
+ }
43
+ write(input) {
44
+ this.process?.write(input);
45
+ }
46
+ stop() {
47
+ if (!this.process) {
48
+ return;
49
+ }
50
+ this.process.write("\u0003");
51
+ this.stopTimer = setTimeout(() => {
52
+ this.process?.kill();
53
+ }, 1500);
54
+ this.stopTimer.unref();
55
+ }
56
+ onData(listener) {
57
+ this.dataListeners.add(listener);
58
+ return () => {
59
+ this.dataListeners.delete(listener);
60
+ };
61
+ }
62
+ onExit(listener) {
63
+ this.exitListeners.add(listener);
64
+ return () => {
65
+ this.exitListeners.delete(listener);
66
+ };
67
+ }
68
+ isAlive() {
69
+ return this.process !== null;
70
+ }
71
+ }
72
+ exports.PtyRunner = PtyRunner;