@doguyilmaz/konvoy 0.3.1 → 0.3.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doguyilmaz/konvoy",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "One session across Claude Code, Codex, Kiro CLI and opencode: bind, hand off, fail over.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/cli.ts CHANGED
@@ -22,7 +22,7 @@ import { cmdRename } from './commands/rename'
22
22
  import { cmdUsage } from './commands/usage'
23
23
  import { cmdDashboard } from './commands/dashboard'
24
24
  import { formatCommandList, resolveCommandName, type CommandName } from './commands/table'
25
- import { runRepl, startSession, type ReplIo } from './commands/repl'
25
+ import { runRepl, startSession, terminalIo, type ReplIo } from './commands/repl'
26
26
  import type { AgentId } from './types'
27
27
  import pkg from '../package.json'
28
28
 
@@ -115,15 +115,7 @@ const handlers: Record<CommandName, Handler> = {
115
115
  }),
116
116
  }
117
117
 
118
- const stdio = (): ReplIo => ({
119
- lines: console as unknown as AsyncIterable<string>,
120
- write: (text) => {
121
- process.stdout.write(text)
122
- },
123
- tty: Boolean(process.stdin.isTTY),
124
- })
125
-
126
- export async function main(argv: string[], io: ReplIo = stdio()): Promise<number> {
118
+ export async function main(argv: string[], io?: ReplIo): Promise<number> {
127
119
  const args = parseArgs(argv)
128
120
  const [command, ...rest] = args._
129
121
  const cwd = process.cwd()
@@ -172,7 +164,7 @@ async function dispatch(command: string, rest: string[], cwd: string, slug: stri
172
164
  return handlers[name]({ db, cfg, cwd, args, slug }, rest)
173
165
  }
174
166
 
175
- async function interactive(io: ReplIo, cwd: string, slug: string | undefined): Promise<number> {
167
+ async function interactive(given: ReplIo | undefined, cwd: string, slug: string | undefined): Promise<number> {
176
168
  const db = openDb(dbPath())
177
169
  const cfg = await loadConfig({ cwd: (slug ? getSessionBySlug(db, slug)?.cwd : undefined) ?? cwd })
178
170
  const session = await startSession(db, cfg, cwd, slug)
@@ -180,16 +172,21 @@ async function interactive(io: ReplIo, cwd: string, slug: string | undefined): P
180
172
  console.error(slug ? `no konvoy session named "${slug}"` : 'could not create a session here')
181
173
  return 2
182
174
  }
183
- return runRepl(io, db, cfg, cwd, session, (tokens, current) => {
184
- const a = parseArgs(tokens)
185
- const [cmd = '', ...r] = a._
186
- const name = resolveCommandName(cmd)
187
- if (!name) {
188
- console.error(`unknown command "/${cmd}" - /help lists them`)
189
- return 2
190
- }
191
- return handlers[name]({ db, cfg, cwd, args: a, slug: current }, r)
192
- })
175
+ const io = given ?? terminalIo()
176
+ try {
177
+ return await runRepl(io, db, cfg, cwd, session, (tokens, current) => {
178
+ const a = parseArgs(tokens)
179
+ const [cmd = '', ...r] = a._
180
+ const name = resolveCommandName(cmd)
181
+ if (!name) {
182
+ console.error(`unknown command "/${cmd}" - /help lists them`)
183
+ return 2
184
+ }
185
+ return handlers[name]({ db, cfg, cwd, args: a, slug: current }, r)
186
+ })
187
+ } finally {
188
+ io.pause()
189
+ }
193
190
  }
194
191
 
195
192
  if (import.meta.main) {
@@ -12,6 +12,51 @@ export interface ReplIo {
12
12
  lines: AsyncIterable<string>
13
13
  write: (text: string) => void
14
14
  tty: boolean
15
+ /** stop reading stdin while a command runs, so a child that inherits the terminal gets every keystroke */
16
+ pause: () => void
17
+ resume: () => void
18
+ }
19
+
20
+ export function terminalIo(): ReplIo {
21
+ const queue: string[] = []
22
+ const decoder = new TextDecoder()
23
+ let buffered = ''
24
+ let ended = false
25
+ let wake: (() => void) | undefined
26
+ const flush = (): void => {
27
+ wake?.()
28
+ wake = undefined
29
+ }
30
+ process.stdin.on('data', (chunk: Uint8Array) => {
31
+ buffered += decoder.decode(chunk, { stream: true })
32
+ let at: number
33
+ while ((at = buffered.indexOf('\n')) >= 0) {
34
+ queue.push(buffered.slice(0, at))
35
+ buffered = buffered.slice(at + 1)
36
+ }
37
+ flush()
38
+ })
39
+ process.stdin.on('end', () => {
40
+ if (buffered) queue.push(buffered)
41
+ ended = true
42
+ flush()
43
+ })
44
+ async function* lines(): AsyncGenerator<string> {
45
+ for (;;) {
46
+ if (queue.length > 0) yield queue.shift()!
47
+ else if (ended) return
48
+ else await new Promise<void>((resolve) => (wake = resolve))
49
+ }
50
+ }
51
+ return {
52
+ lines: lines(),
53
+ write: (text) => {
54
+ process.stdout.write(text)
55
+ },
56
+ tty: Boolean(process.stdin.isTTY),
57
+ pause: () => process.stdin.pause(),
58
+ resume: () => process.stdin.resume(),
59
+ }
15
60
  }
16
61
 
17
62
  /** runs one command line through the command table, as `konvoy <tokens>` would, for the given session */
@@ -52,6 +97,15 @@ export async function runRepl(
52
97
  return fresh !== undefined
53
98
  }
54
99
 
100
+ const exec = async (tokens: string[]): Promise<void> => {
101
+ io.pause()
102
+ try {
103
+ await run(tokens, session.slug)
104
+ } finally {
105
+ io.resume()
106
+ }
107
+ }
108
+
55
109
  prompt()
56
110
  for await (const raw of io.lines) {
57
111
  const line = raw.trim()
@@ -60,7 +114,7 @@ export async function runRepl(
60
114
  continue
61
115
  }
62
116
  if (!line.startsWith('/')) {
63
- await run(['send', agent, line], session.slug)
117
+ await exec(['send', agent, line])
64
118
  // failover never falls back, so the agent that answered is the one to keep talking to
65
119
  const moved = lastTurnAgent(db, session.id)
66
120
  if (moved && moved !== agent) agent = moved
@@ -94,13 +148,13 @@ export async function runRepl(
94
148
  } else if (cmd === 'rename') {
95
149
  if (rest.length === 0) console.error('usage: /rename <new-name>')
96
150
  else {
97
- await run(['rename', session.slug, rest.join(' ')], session.slug)
151
+ await exec(['rename', session.slug, rest.join(' ')])
98
152
  refresh()
99
153
  }
100
154
  } else if (cmd === 'attach') {
101
- await run(['attach', rest[0] ?? agent, ...rest.slice(1)], session.slug)
155
+ await exec(['attach', rest[0] ?? agent, ...rest.slice(1)])
102
156
  } else {
103
- await run([cmd, ...rest], session.slug)
157
+ await exec([cmd, ...rest])
104
158
  if (cmd === 'resume' && rest[0]) {
105
159
  const target = getSessionBySlug(db, rest[0])
106
160
  if (target) {