@mylesiyabor/claudex 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/lib/native-agents.js +1 -1
- package/lib/session-store.js +57 -1
- package/lib/ui.js +106 -25
- package/package.json +2 -2
package/lib/native-agents.js
CHANGED
|
@@ -107,7 +107,7 @@ function runProcess(command, args, cwd, handlers = {}) {
|
|
|
107
107
|
handlers.onStart?.();
|
|
108
108
|
|
|
109
109
|
const promise = new Promise((resolve, reject) => {
|
|
110
|
-
|
|
110
|
+
child = spawn(command, args, {
|
|
111
111
|
cwd,
|
|
112
112
|
env: process.env,
|
|
113
113
|
stdio: ['ignore', 'pipe', 'pipe'],
|
package/lib/session-store.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { appendFileSync, createReadStream, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { appendFileSync, copyFileSync, createReadStream, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
|
|
@@ -97,6 +97,8 @@ export async function importClaudeSessionToCodex({ session, cwd = process.cwd()
|
|
|
97
97
|
updatedAt: lastTimestamp || createdAt,
|
|
98
98
|
});
|
|
99
99
|
|
|
100
|
+
syncClaudeMemoriesToCodex();
|
|
101
|
+
|
|
100
102
|
return {
|
|
101
103
|
sessionId,
|
|
102
104
|
sessionPath,
|
|
@@ -107,6 +109,29 @@ export async function importClaudeSessionToCodex({ session, cwd = process.cwd()
|
|
|
107
109
|
};
|
|
108
110
|
}
|
|
109
111
|
|
|
112
|
+
export function syncClaudeMemoriesToCodex() {
|
|
113
|
+
const targetRoot = join(CODEX_HOME, 'memories', 'claude');
|
|
114
|
+
mkdirSync(targetRoot, { recursive: true });
|
|
115
|
+
|
|
116
|
+
let copied = 0;
|
|
117
|
+
const sources = [
|
|
118
|
+
{ root: join(homedir(), '.claude', 'memory'), prefix: 'global' },
|
|
119
|
+
{ root: CLAUDE_PROJECTS, prefix: 'projects' },
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
for (const source of sources) {
|
|
123
|
+
copied += copyMarkdownTree(source.root, join(targetRoot, source.prefix));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
writeFileSync(
|
|
127
|
+
join(targetRoot, 'README.md'),
|
|
128
|
+
`# Claude Memories\n\nImported from ~/.claude on ${new Date().toISOString()}.\n\nFiles copied: ${copied}\n`,
|
|
129
|
+
'utf8',
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
return copied;
|
|
133
|
+
}
|
|
134
|
+
|
|
110
135
|
function resolveClaudeSessionPath(sessionRef) {
|
|
111
136
|
if (sessionRef.endsWith('.jsonl')) {
|
|
112
137
|
const explicitPath = isAbsolute(sessionRef) ? sessionRef : resolve(process.cwd(), sessionRef);
|
|
@@ -196,6 +221,37 @@ function listClaudeSessions(limit) {
|
|
|
196
221
|
.slice(0, limit);
|
|
197
222
|
}
|
|
198
223
|
|
|
224
|
+
function copyMarkdownTree(sourceDir, targetDir) {
|
|
225
|
+
if (!existsSync(sourceDir)) return 0;
|
|
226
|
+
|
|
227
|
+
let copied = 0;
|
|
228
|
+
const stack = [{ sourceDir, targetDir }];
|
|
229
|
+
while (stack.length > 0) {
|
|
230
|
+
const current = stack.pop();
|
|
231
|
+
let entries;
|
|
232
|
+
try {
|
|
233
|
+
entries = readdirSync(current.sourceDir, { withFileTypes: true });
|
|
234
|
+
} catch {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
for (const entry of entries) {
|
|
239
|
+
const src = join(current.sourceDir, entry.name);
|
|
240
|
+
const dest = join(current.targetDir, entry.name);
|
|
241
|
+
if (entry.isDirectory()) {
|
|
242
|
+
stack.push({ sourceDir: src, targetDir: dest });
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
|
|
246
|
+
mkdirSync(current.targetDir, { recursive: true });
|
|
247
|
+
copyFileSync(src, dest);
|
|
248
|
+
copied += 1;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return copied;
|
|
253
|
+
}
|
|
254
|
+
|
|
199
255
|
function readClaudeSessionSummary(path) {
|
|
200
256
|
let title = '(untitled)';
|
|
201
257
|
let updatedAt = null;
|
package/lib/ui.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
import React, { useMemo, useState } from 'react';
|
|
1
|
+
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import { Box, Text, useApp, useInput, useStdout } from 'ink';
|
|
3
3
|
import Spinner from 'ink-spinner';
|
|
4
4
|
import { MessageComponent } from 'terminal-chat-ui';
|
|
5
|
-
import {
|
|
6
|
-
import { hasCodexSession, importClaudeSessionToCodex, listResumeSessions } from './session-store.js';
|
|
5
|
+
import { startClaudePrompt, startCodexPrompt } from './native-agents.js';
|
|
6
|
+
import { hasCodexSession, importClaudeSessionToCodex, listResumeSessions, syncClaudeMemoriesToCodex } from './session-store.js';
|
|
7
7
|
|
|
8
8
|
const HELP_LINES = [
|
|
9
9
|
'/use claude | /use codex',
|
|
10
10
|
'/resume --cli <claude|codex> <session-id-or-claude-jsonl-path>',
|
|
11
11
|
'/ids',
|
|
12
12
|
'/menu',
|
|
13
|
+
'/sync-memory',
|
|
14
|
+
'/stop',
|
|
13
15
|
'/clear',
|
|
14
16
|
'/help',
|
|
15
17
|
'/exit',
|
|
@@ -24,6 +26,11 @@ export function ClauDexApp({ initialAgent = 'codex', initialSessions, initialNot
|
|
|
24
26
|
messageOf('system', initialNotice || 'Type /help for commands.'),
|
|
25
27
|
]);
|
|
26
28
|
const [busy, setBusy] = useState(false);
|
|
29
|
+
const [queuedMessages, setQueuedMessages] = useState([]);
|
|
30
|
+
const [activity, setActivity] = useState('idle');
|
|
31
|
+
const [elapsedSec, setElapsedSec] = useState(0);
|
|
32
|
+
const activeRunRef = useRef(null);
|
|
33
|
+
const startedAtRef = useRef(null);
|
|
27
34
|
const [menu, setMenu] = useState({ open: false, selected: 0, items: [] });
|
|
28
35
|
|
|
29
36
|
const transcriptHeight = Math.max(8, (stdout?.rows || 24) - 7);
|
|
@@ -34,9 +41,32 @@ export function ClauDexApp({ initialAgent = 'codex', initialSessions, initialNot
|
|
|
34
41
|
return `active=${agent} claude=${claudeId} codex=${codexId}`;
|
|
35
42
|
}, [agent, sessions]);
|
|
36
43
|
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (!busy) return undefined;
|
|
46
|
+
const timer = setInterval(() => {
|
|
47
|
+
if (startedAtRef.current) {
|
|
48
|
+
setElapsedSec(Math.max(0, Math.floor((Date.now() - startedAtRef.current) / 1000)));
|
|
49
|
+
}
|
|
50
|
+
}, 250);
|
|
51
|
+
return () => clearInterval(timer);
|
|
52
|
+
}, [busy]);
|
|
53
|
+
|
|
54
|
+
useInput((input, key) => {
|
|
55
|
+
if (key.escape && busy && activeRunRef.current) {
|
|
56
|
+
activeRunRef.current.interrupt();
|
|
57
|
+
setActivity('interrupt requested');
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
37
61
|
async function submit(value) {
|
|
38
62
|
const text = value.trim();
|
|
39
|
-
if (!text
|
|
63
|
+
if (!text) return;
|
|
64
|
+
|
|
65
|
+
if (busy && !text.startsWith('/')) {
|
|
66
|
+
setQueuedMessages((current) => [...current, { agent, text }]);
|
|
67
|
+
setMessages((current) => [...current, messageOf('system', `Queued for ${agent}: ${text}`)]);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
40
70
|
|
|
41
71
|
setMessages((current) => [...current, messageOf('user', `[to ${agent}] ${text}`)]);
|
|
42
72
|
|
|
@@ -47,34 +77,60 @@ export function ClauDexApp({ initialAgent = 'codex', initialSessions, initialNot
|
|
|
47
77
|
setSessions,
|
|
48
78
|
setMessages,
|
|
49
79
|
setMenu,
|
|
80
|
+
interrupt: () => activeRunRef.current?.interrupt(),
|
|
81
|
+
setActivity,
|
|
50
82
|
exit,
|
|
51
83
|
});
|
|
52
84
|
return;
|
|
53
85
|
}
|
|
54
86
|
|
|
87
|
+
await runPrompt(agent, text);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function runPrompt(targetAgent, text) {
|
|
55
91
|
setBusy(true);
|
|
92
|
+
setElapsedSec(0);
|
|
93
|
+
setActivity(`${targetAgent} starting`);
|
|
94
|
+
startedAtRef.current = Date.now();
|
|
95
|
+
|
|
96
|
+
const run = targetAgent === 'claude'
|
|
97
|
+
? startClaudePrompt({
|
|
98
|
+
prompt: text,
|
|
99
|
+
sessionId: sessions.claude,
|
|
100
|
+
cwd: process.cwd(),
|
|
101
|
+
onActivity: setActivity,
|
|
102
|
+
})
|
|
103
|
+
: startCodexPrompt({
|
|
104
|
+
prompt: text,
|
|
105
|
+
sessionId: sessions.codex,
|
|
106
|
+
cwd: process.cwd(),
|
|
107
|
+
onActivity: setActivity,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
activeRunRef.current = run;
|
|
111
|
+
|
|
56
112
|
try {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
sessionId: sessions.claude,
|
|
61
|
-
cwd: process.cwd(),
|
|
62
|
-
});
|
|
63
|
-
setSessions((current) => ({ ...current, claude: result.sessionId }));
|
|
64
|
-
setMessages((current) => [...current, messageOf('assistant', `[claude]\n${result.text}`)]);
|
|
65
|
-
} else {
|
|
66
|
-
const result = await sendCodexPrompt({
|
|
67
|
-
prompt: text,
|
|
68
|
-
sessionId: sessions.codex,
|
|
69
|
-
cwd: process.cwd(),
|
|
70
|
-
});
|
|
71
|
-
setSessions((current) => ({ ...current, codex: result.sessionId }));
|
|
72
|
-
setMessages((current) => [...current, messageOf('assistant', `[codex]\n${result.text}`)]);
|
|
73
|
-
}
|
|
113
|
+
const result = await run.promise;
|
|
114
|
+
setSessions((current) => ({ ...current, [targetAgent]: result.sessionId }));
|
|
115
|
+
setMessages((current) => [...current, messageOf('assistant', `[${targetAgent}]\n${result.text}`)]);
|
|
74
116
|
} catch (error) {
|
|
75
117
|
setMessages((current) => [...current, messageOf('error', error?.message || String(error))]);
|
|
76
118
|
} finally {
|
|
119
|
+
activeRunRef.current = null;
|
|
120
|
+
startedAtRef.current = null;
|
|
77
121
|
setBusy(false);
|
|
122
|
+
setElapsedSec(0);
|
|
123
|
+
setActivity('idle');
|
|
124
|
+
setQueuedMessages((current) => {
|
|
125
|
+
const [next, ...rest] = current;
|
|
126
|
+
if (next) {
|
|
127
|
+
setTimeout(() => {
|
|
128
|
+
setMessages((existing) => [...existing, messageOf('user', `[to ${next.agent}] ${next.text}`)]);
|
|
129
|
+
runPrompt(next.agent, next.text);
|
|
130
|
+
}, 0);
|
|
131
|
+
}
|
|
132
|
+
return rest;
|
|
133
|
+
});
|
|
78
134
|
}
|
|
79
135
|
}
|
|
80
136
|
|
|
@@ -91,7 +147,13 @@ export function ClauDexApp({ initialAgent = 'codex', initialSessions, initialNot
|
|
|
91
147
|
},
|
|
92
148
|
React.createElement(Text, { color: accent, bold: true }, 'ClauDex'),
|
|
93
149
|
React.createElement(Text, { dimColor: true }, status),
|
|
94
|
-
React.createElement(
|
|
150
|
+
React.createElement(
|
|
151
|
+
Text,
|
|
152
|
+
{ color: busy ? 'yellow' : 'green' },
|
|
153
|
+
busy
|
|
154
|
+
? `Working (${formatElapsed(elapsedSec)} • esc to interrupt • q:${queuedMessages.length})`
|
|
155
|
+
: `ready • q:${queuedMessages.length}`,
|
|
156
|
+
),
|
|
95
157
|
),
|
|
96
158
|
React.createElement(
|
|
97
159
|
Box,
|
|
@@ -125,7 +187,7 @@ export function ClauDexApp({ initialAgent = 'codex', initialSessions, initialNot
|
|
|
125
187
|
React.createElement(Text, { color: accent }, 'agent '),
|
|
126
188
|
React.createElement(Text, { color: accent },
|
|
127
189
|
React.createElement(Spinner, { type: 'dots' }),
|
|
128
|
-
|
|
190
|
+
` ${activity}`,
|
|
129
191
|
),
|
|
130
192
|
)
|
|
131
193
|
: null,
|
|
@@ -148,9 +210,9 @@ export function ClauDexApp({ initialAgent = 'codex', initialSessions, initialNot
|
|
|
148
210
|
},
|
|
149
211
|
React.createElement(Text, { color: accent, bold: true }, `${agent} › `),
|
|
150
212
|
React.createElement(Composer, {
|
|
151
|
-
placeholder: busy ? '
|
|
213
|
+
placeholder: busy ? 'queue next message or /stop' : 'message or /help',
|
|
152
214
|
onSubmit: submit,
|
|
153
|
-
disabled:
|
|
215
|
+
disabled: menu.open,
|
|
154
216
|
}),
|
|
155
217
|
),
|
|
156
218
|
);
|
|
@@ -192,6 +254,19 @@ async function runCommand(text, ctx) {
|
|
|
192
254
|
return;
|
|
193
255
|
}
|
|
194
256
|
|
|
257
|
+
if (command === '/sync-memory') {
|
|
258
|
+
const copied = syncClaudeMemoriesToCodex();
|
|
259
|
+
ctx.setMessages((current) => [...current, messageOf('system', `Synced ${copied} Claude memory markdown files into ~/.codex/memories/claude`)]);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (command === '/stop') {
|
|
264
|
+
ctx.interrupt?.();
|
|
265
|
+
ctx.setActivity?.('interrupt requested');
|
|
266
|
+
ctx.setMessages((current) => [...current, messageOf('system', 'Interrupt requested.')]);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
195
270
|
if (command === '/use') {
|
|
196
271
|
const next = args[0];
|
|
197
272
|
if (!['claude', 'codex'].includes(next)) {
|
|
@@ -281,6 +356,12 @@ function messageOf(role, content) {
|
|
|
281
356
|
};
|
|
282
357
|
}
|
|
283
358
|
|
|
359
|
+
function formatElapsed(totalSeconds) {
|
|
360
|
+
const mins = String(Math.floor(totalSeconds / 60));
|
|
361
|
+
const secs = String(totalSeconds % 60).padStart(2, '0');
|
|
362
|
+
return `${mins}:${secs}`;
|
|
363
|
+
}
|
|
364
|
+
|
|
284
365
|
function Composer({ placeholder, onSubmit, disabled }) {
|
|
285
366
|
const [value, setValue] = useState('');
|
|
286
367
|
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mylesiyabor/claudex",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Ink TUI for chatting with Claude Code and Codex CLI from one switchable interface.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"claudex": "
|
|
7
|
+
"claudex": "bin/claudex.js"
|
|
8
8
|
},
|
|
9
9
|
"engines": {
|
|
10
10
|
"node": ">=20.0.0"
|