remcodex 0.1.0-beta.1 → 0.1.0-beta.11
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/README.md +30 -39
- package/dist/server/src/app.js +3 -26
- package/dist/server/src/cli.js +65 -7
- package/dist/server/src/controllers/session.controller.js +12 -0
- package/dist/server/src/db/migrations.js +30 -1
- package/dist/server/src/db/schema.sql +48 -0
- package/dist/server/src/services/codex-rollout-sync.js +5 -16
- package/dist/server/src/services/event-store.js +28 -5
- package/dist/server/src/services/session-manager.js +79 -6
- package/dist/server/src/services/session-timeline-service.js +7 -169
- package/dist/server/src/utils/output-limits.js +73 -0
- package/dist/server/src/utils/runtime-paths.js +31 -0
- package/docs/assets/approval-flow.png +0 -0
- package/docs/assets/hero-desktop.png +0 -0
- package/docs/assets/imported-session.png +0 -0
- package/docs/assets/mobile-session.png +0 -0
- package/package.json +13 -5
- package/scripts/check-node-version.js +14 -0
- package/web/api.js +7 -0
- package/web/app.js +241 -37
- package/web/i18n/locales/en.js +3 -2
- package/web/i18n/locales/zh-CN.js +3 -2
- package/web/session-timeline-reducer.js +66 -13
- package/web/styles.css +22 -0
- package/LICENSE +0 -21
|
@@ -1,181 +1,19 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.SessionTimelineService = void 0;
|
|
4
|
-
const DEFAULT_TIMELINE_LIMIT = 200;
|
|
5
|
-
const MAX_TIMELINE_LIMIT = 400;
|
|
6
|
-
function clampLimit(limit) {
|
|
7
|
-
const numeric = Number(limit || DEFAULT_TIMELINE_LIMIT);
|
|
8
|
-
if (!Number.isFinite(numeric) || numeric <= 0) {
|
|
9
|
-
return DEFAULT_TIMELINE_LIMIT;
|
|
10
|
-
}
|
|
11
|
-
return Math.max(1, Math.min(Math.trunc(numeric), MAX_TIMELINE_LIMIT));
|
|
12
|
-
}
|
|
13
|
-
function normalizeCursor(value) {
|
|
14
|
-
const numeric = Number(value || 0);
|
|
15
|
-
if (!Number.isFinite(numeric) || numeric <= 0) {
|
|
16
|
-
return 0;
|
|
17
|
-
}
|
|
18
|
-
return Math.trunc(numeric);
|
|
19
|
-
}
|
|
20
|
-
function cloneEvent(event) {
|
|
21
|
-
return {
|
|
22
|
-
...event,
|
|
23
|
-
payload: event.payload && typeof event.payload === "object"
|
|
24
|
-
? { ...event.payload }
|
|
25
|
-
: event.payload,
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
function appendTextDelta(currentValue, nextValue) {
|
|
29
|
-
if (!nextValue) {
|
|
30
|
-
return currentValue || "";
|
|
31
|
-
}
|
|
32
|
-
return `${currentValue || ""}${nextValue}`;
|
|
33
|
-
}
|
|
34
|
-
function compareEvents(left, right) {
|
|
35
|
-
if (left.seq !== right.seq) {
|
|
36
|
-
return left.seq - right.seq;
|
|
37
|
-
}
|
|
38
|
-
return String(left.id || "").localeCompare(String(right.id || ""));
|
|
39
|
-
}
|
|
40
|
-
function upsertTimelineEvent(items, indexById, nextEvent) {
|
|
41
|
-
const existing = indexById.get(nextEvent.id);
|
|
42
|
-
if (existing) {
|
|
43
|
-
Object.assign(existing, nextEvent);
|
|
44
|
-
return existing;
|
|
45
|
-
}
|
|
46
|
-
const cloned = cloneEvent(nextEvent);
|
|
47
|
-
items.push(cloned);
|
|
48
|
-
indexById.set(cloned.id, cloned);
|
|
49
|
-
return cloned;
|
|
50
|
-
}
|
|
51
|
-
function timelineAssistantDeltaId(event) {
|
|
52
|
-
return `timeline:assistant:delta:${event.messageId || event.id}`;
|
|
53
|
-
}
|
|
54
|
-
function timelineReasoningDeltaId(event) {
|
|
55
|
-
return `timeline:reasoning:delta:${event.messageId || event.id}`;
|
|
56
|
-
}
|
|
57
|
-
function timelineCommandOutputId(event) {
|
|
58
|
-
return `timeline:command:output:${event.callId || event.id}:${event.stream || "stdout"}`;
|
|
59
|
-
}
|
|
60
|
-
function timelinePatchOutputId(event) {
|
|
61
|
-
return `timeline:patch:output:${event.callId || event.id}`;
|
|
62
|
-
}
|
|
63
|
-
function aggregateSemanticTimeline(rawEvents) {
|
|
64
|
-
const items = [];
|
|
65
|
-
const indexById = new Map();
|
|
66
|
-
rawEvents.forEach((event) => {
|
|
67
|
-
switch (event.type) {
|
|
68
|
-
case "message.assistant.delta": {
|
|
69
|
-
const syntheticId = timelineAssistantDeltaId(event);
|
|
70
|
-
const existing = indexById.get(syntheticId);
|
|
71
|
-
upsertTimelineEvent(items, indexById, {
|
|
72
|
-
...cloneEvent(event),
|
|
73
|
-
id: syntheticId,
|
|
74
|
-
payload: {
|
|
75
|
-
...(event.payload || {}),
|
|
76
|
-
textDelta: appendTextDelta(existing?.payload?.textDelta || "", String(event.payload?.textDelta || "")),
|
|
77
|
-
},
|
|
78
|
-
});
|
|
79
|
-
break;
|
|
80
|
-
}
|
|
81
|
-
case "reasoning.delta": {
|
|
82
|
-
const syntheticId = timelineReasoningDeltaId(event);
|
|
83
|
-
const existing = indexById.get(syntheticId);
|
|
84
|
-
const nextText = appendTextDelta(existing?.payload?.textDelta || "", String(event.payload?.textDelta || ""));
|
|
85
|
-
upsertTimelineEvent(items, indexById, {
|
|
86
|
-
...cloneEvent(event),
|
|
87
|
-
id: syntheticId,
|
|
88
|
-
payload: {
|
|
89
|
-
...(event.payload || {}),
|
|
90
|
-
textDelta: nextText,
|
|
91
|
-
summary: event.payload?.summary ||
|
|
92
|
-
existing?.payload?.summary ||
|
|
93
|
-
nextText ||
|
|
94
|
-
null,
|
|
95
|
-
},
|
|
96
|
-
});
|
|
97
|
-
break;
|
|
98
|
-
}
|
|
99
|
-
case "command.output.delta": {
|
|
100
|
-
const syntheticId = timelineCommandOutputId(event);
|
|
101
|
-
const existing = indexById.get(syntheticId);
|
|
102
|
-
upsertTimelineEvent(items, indexById, {
|
|
103
|
-
...cloneEvent(event),
|
|
104
|
-
id: syntheticId,
|
|
105
|
-
payload: {
|
|
106
|
-
...(event.payload || {}),
|
|
107
|
-
textDelta: appendTextDelta(existing?.payload?.textDelta || "", String(event.payload?.textDelta || "")),
|
|
108
|
-
stream: event.payload?.stream || event.stream || "stdout",
|
|
109
|
-
},
|
|
110
|
-
});
|
|
111
|
-
break;
|
|
112
|
-
}
|
|
113
|
-
case "patch.output.delta": {
|
|
114
|
-
const syntheticId = timelinePatchOutputId(event);
|
|
115
|
-
const existing = indexById.get(syntheticId);
|
|
116
|
-
upsertTimelineEvent(items, indexById, {
|
|
117
|
-
...cloneEvent(event),
|
|
118
|
-
id: syntheticId,
|
|
119
|
-
payload: {
|
|
120
|
-
...(event.payload || {}),
|
|
121
|
-
textDelta: appendTextDelta(existing?.payload?.textDelta || "", String(event.payload?.textDelta || "")),
|
|
122
|
-
},
|
|
123
|
-
});
|
|
124
|
-
break;
|
|
125
|
-
}
|
|
126
|
-
default:
|
|
127
|
-
upsertTimelineEvent(items, indexById, event);
|
|
128
|
-
break;
|
|
129
|
-
}
|
|
130
|
-
});
|
|
131
|
-
return items.sort(compareEvents);
|
|
132
|
-
}
|
|
133
|
-
function paginateTimelineItems(items, options, lastSeq) {
|
|
134
|
-
const limit = clampLimit(options.limit);
|
|
135
|
-
const after = normalizeCursor(options.after);
|
|
136
|
-
const before = normalizeCursor(options.before);
|
|
137
|
-
if (before > 0) {
|
|
138
|
-
const matches = items.filter((item) => item.seq < before);
|
|
139
|
-
const hasMoreBefore = matches.length > limit;
|
|
140
|
-
const pageItems = matches.slice(Math.max(0, matches.length - limit));
|
|
141
|
-
return {
|
|
142
|
-
items: pageItems,
|
|
143
|
-
nextCursor: pageItems.at(-1)?.seq || after,
|
|
144
|
-
beforeCursor: pageItems[0]?.seq || before,
|
|
145
|
-
hasMoreBefore,
|
|
146
|
-
lastSeq,
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
if (after > 0) {
|
|
150
|
-
const pageItems = items.filter((item) => item.seq > after).slice(0, limit);
|
|
151
|
-
return {
|
|
152
|
-
items: pageItems,
|
|
153
|
-
nextCursor: pageItems.at(-1)?.seq || after,
|
|
154
|
-
beforeCursor: pageItems[0]?.seq || 0,
|
|
155
|
-
hasMoreBefore: pageItems.length > 0 ? pageItems[0].seq > 1 : items.length > 0,
|
|
156
|
-
lastSeq,
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
const hasMoreBefore = items.length > limit;
|
|
160
|
-
const pageItems = items.slice(Math.max(0, items.length - limit));
|
|
161
|
-
return {
|
|
162
|
-
items: pageItems,
|
|
163
|
-
nextCursor: pageItems.at(-1)?.seq || 0,
|
|
164
|
-
beforeCursor: pageItems[0]?.seq || 0,
|
|
165
|
-
hasMoreBefore,
|
|
166
|
-
lastSeq,
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
4
|
class SessionTimelineService {
|
|
170
5
|
eventStore;
|
|
171
6
|
constructor(eventStore) {
|
|
172
7
|
this.eventStore = eventStore;
|
|
173
8
|
}
|
|
174
9
|
list(sessionId, options = {}) {
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
10
|
+
const page = this.eventStore.list(sessionId, options);
|
|
11
|
+
return {
|
|
12
|
+
...page,
|
|
13
|
+
// The initial detail load only needs the latest observed seq so resume sync
|
|
14
|
+
// can continue from the newest page we fetched.
|
|
15
|
+
lastSeq: page.nextCursor || Math.max(0, Number(options.after || 0)),
|
|
16
|
+
};
|
|
179
17
|
}
|
|
180
18
|
}
|
|
181
19
|
exports.SessionTimelineService = SessionTimelineService;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.COMMAND_STREAM_TRUNCATION_NOTICE = exports.MAX_PERSISTED_COMMAND_STREAM_CHARS = void 0;
|
|
4
|
+
exports.appendCappedText = appendCappedText;
|
|
5
|
+
exports.capTextValue = capTextValue;
|
|
6
|
+
exports.MAX_PERSISTED_COMMAND_STREAM_CHARS = 80 * 1024;
|
|
7
|
+
exports.COMMAND_STREAM_TRUNCATION_NOTICE = "\n\n[command output truncated]\n";
|
|
8
|
+
function normalizeMaxChars(maxChars, notice) {
|
|
9
|
+
const numeric = Number(maxChars || exports.MAX_PERSISTED_COMMAND_STREAM_CHARS);
|
|
10
|
+
if (!Number.isFinite(numeric) || numeric <= notice.length + 1) {
|
|
11
|
+
return exports.MAX_PERSISTED_COMMAND_STREAM_CHARS;
|
|
12
|
+
}
|
|
13
|
+
return Math.trunc(numeric);
|
|
14
|
+
}
|
|
15
|
+
function appendCappedText(currentText, nextDelta, options = {}) {
|
|
16
|
+
const notice = String(options.notice || exports.COMMAND_STREAM_TRUNCATION_NOTICE);
|
|
17
|
+
const safeCurrent = String(currentText || "");
|
|
18
|
+
const safeDelta = String(nextDelta || "");
|
|
19
|
+
if (!safeDelta) {
|
|
20
|
+
return {
|
|
21
|
+
nextText: safeCurrent,
|
|
22
|
+
appendedText: "",
|
|
23
|
+
truncated: false,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const maxChars = normalizeMaxChars(options.maxChars, notice);
|
|
27
|
+
const contentLimit = Math.max(0, maxChars - notice.length);
|
|
28
|
+
if (safeCurrent.endsWith(notice) || safeCurrent.length >= maxChars) {
|
|
29
|
+
return {
|
|
30
|
+
nextText: safeCurrent.length > maxChars ? safeCurrent.slice(0, maxChars) : safeCurrent,
|
|
31
|
+
appendedText: "",
|
|
32
|
+
truncated: true,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
if (safeCurrent.length >= contentLimit) {
|
|
36
|
+
return {
|
|
37
|
+
nextText: `${safeCurrent.slice(0, contentLimit)}${notice}`,
|
|
38
|
+
appendedText: notice,
|
|
39
|
+
truncated: true,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
if (safeCurrent.length + safeDelta.length <= contentLimit) {
|
|
43
|
+
return {
|
|
44
|
+
nextText: safeCurrent + safeDelta,
|
|
45
|
+
appendedText: safeDelta,
|
|
46
|
+
truncated: false,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const available = Math.max(0, contentLimit - safeCurrent.length);
|
|
50
|
+
const preserved = safeDelta.slice(0, available);
|
|
51
|
+
const appendedText = `${preserved}${notice}`;
|
|
52
|
+
return {
|
|
53
|
+
nextText: `${safeCurrent}${appendedText}`,
|
|
54
|
+
appendedText,
|
|
55
|
+
truncated: true,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function capTextValue(text, options = {}) {
|
|
59
|
+
const notice = String(options.notice || exports.COMMAND_STREAM_TRUNCATION_NOTICE);
|
|
60
|
+
const safeText = String(text || "");
|
|
61
|
+
const maxChars = normalizeMaxChars(options.maxChars, notice);
|
|
62
|
+
const contentLimit = Math.max(0, maxChars - notice.length);
|
|
63
|
+
if (safeText.length <= contentLimit) {
|
|
64
|
+
return {
|
|
65
|
+
text: safeText,
|
|
66
|
+
truncated: false,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
text: `${safeText.slice(0, contentLimit)}${notice}`,
|
|
71
|
+
truncated: true,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
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.resolvePackageRoot = resolvePackageRoot;
|
|
7
|
+
exports.resolveDefaultDatabasePath = resolveDefaultDatabasePath;
|
|
8
|
+
const node_fs_1 = require("node:fs");
|
|
9
|
+
const node_os_1 = require("node:os");
|
|
10
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
+
function isPackageRoot(root) {
|
|
12
|
+
return ((0, node_fs_1.existsSync)(node_path_1.default.join(root, "package.json")) &&
|
|
13
|
+
(0, node_fs_1.existsSync)(node_path_1.default.join(root, "web", "index.html")));
|
|
14
|
+
}
|
|
15
|
+
function resolvePackageRoot(startDir = __dirname) {
|
|
16
|
+
let current = node_path_1.default.resolve(startDir);
|
|
17
|
+
while (true) {
|
|
18
|
+
if (isPackageRoot(current)) {
|
|
19
|
+
return current;
|
|
20
|
+
}
|
|
21
|
+
const parent = node_path_1.default.dirname(current);
|
|
22
|
+
if (parent === current) {
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
current = parent;
|
|
26
|
+
}
|
|
27
|
+
return process.cwd();
|
|
28
|
+
}
|
|
29
|
+
function resolveDefaultDatabasePath() {
|
|
30
|
+
return node_path_1.default.join((0, node_os_1.homedir)(), ".remcodex", "remcodex.db");
|
|
31
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,23 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "remcodex",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.11",
|
|
4
4
|
"description": "Control Codex from anywhere. Even on your phone.",
|
|
5
|
-
"license": "
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": "20.x"
|
|
8
|
+
},
|
|
6
9
|
"bin": {
|
|
7
10
|
"remcodex": "dist/server/src/cli.js"
|
|
8
11
|
},
|
|
9
12
|
"files": [
|
|
10
13
|
"dist",
|
|
11
14
|
"web",
|
|
15
|
+
"scripts/check-node-version.js",
|
|
12
16
|
"scripts/fix-node-pty-helper.js",
|
|
13
|
-
"README.md"
|
|
17
|
+
"README.md",
|
|
18
|
+
"docs/assets"
|
|
14
19
|
],
|
|
15
20
|
"scripts": {
|
|
21
|
+
"preinstall": "node scripts/check-node-version.js",
|
|
16
22
|
"postinstall": "node scripts/fix-node-pty-helper.js",
|
|
17
23
|
"dev": "tsx watch server/src/app.ts",
|
|
18
24
|
"start": "tsx server/src/cli.ts --no-open",
|
|
19
|
-
"build": "tsc -p tsconfig.json",
|
|
20
|
-
"cli": "tsx server/src/cli.ts --no-open"
|
|
25
|
+
"build": "tsc -p tsconfig.json && node scripts/copy-db-assets.js",
|
|
26
|
+
"cli": "tsx server/src/cli.ts --no-open",
|
|
27
|
+
"smoke:tarball": "node scripts/smoke-test-tarball.js",
|
|
28
|
+
"smoke:start": "node scripts/smoke-start-tarball.js"
|
|
21
29
|
},
|
|
22
30
|
"dependencies": {
|
|
23
31
|
"better-sqlite3": "^11.8.1",
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const supportedMajor = 20;
|
|
2
|
+
const currentVersion = process.versions.node;
|
|
3
|
+
const currentMajor = Number.parseInt(currentVersion.split(".")[0] || "", 10);
|
|
4
|
+
|
|
5
|
+
if (currentMajor !== supportedMajor) {
|
|
6
|
+
console.error(
|
|
7
|
+
[
|
|
8
|
+
`remcodex requires Node.js ${supportedMajor}.x for the published package.`,
|
|
9
|
+
`Current Node.js: ${currentVersion}`,
|
|
10
|
+
"Switch Node versions first, then reinstall remcodex so native modules are built against the same runtime.",
|
|
11
|
+
].join("\n"),
|
|
12
|
+
);
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
package/web/api.js
CHANGED
|
@@ -138,6 +138,13 @@ export function resolveSessionApproval(sessionId, requestId, decision) {
|
|
|
138
138
|
});
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
export function retrySessionApproval(sessionId, requestId, payload = {}) {
|
|
142
|
+
return request(`/api/sessions/${sessionId}/approvals/${encodeURIComponent(requestId)}/retry`, {
|
|
143
|
+
method: "POST",
|
|
144
|
+
body: JSON.stringify(payload),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
141
148
|
export function getHealth() {
|
|
142
149
|
return request("/health");
|
|
143
150
|
}
|