@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.
@@ -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
- const child = spawn(command, args, {
110
+ child = spawn(command, args, {
111
111
  cwd,
112
112
  env: process.env,
113
113
  stdio: ['ignore', 'pipe', 'pipe'],
@@ -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 { sendClaudePrompt, sendCodexPrompt } from './native-agents.js';
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 || busy) return;
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
- if (agent === 'claude') {
58
- const result = await sendClaudePrompt({
59
- prompt: text,
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(Text, { color: busy ? 'yellow' : 'green' }, busy ? 'running' : 'ready'),
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
- ' thinking...',
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 ? 'waiting for response...' : 'message or /help',
213
+ placeholder: busy ? 'queue next message or /stop' : 'message or /help',
152
214
  onSubmit: submit,
153
- disabled: busy || menu.open,
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.0",
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": "./bin/claudex.js"
7
+ "claudex": "bin/claudex.js"
8
8
  },
9
9
  "engines": {
10
10
  "node": ">=20.0.0"