@doguyilmaz/konvoy 0.3.0 → 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.0",
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/args.ts CHANGED
@@ -5,7 +5,7 @@ export interface Args {
5
5
 
6
6
  // Flags that are switches, so `konvoy rm --yes <slug>` keeps its slug. Their consumers are the
7
7
  // `=== true` checks in cli.ts; a value flag (--session, --port) is anything not listed here.
8
- const SWITCHES = new Set(['all', 'yes', 'global', 'chart', 'help'])
8
+ const SWITCHES = new Set(['all', 'yes', 'global', 'chart', 'help', 'h', 'version', 'v', 'V'])
9
9
 
10
10
  export function parseArgs(argv: string[]): Args {
11
11
  const positional: string[] = []
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
 
@@ -35,7 +35,7 @@ export const USAGE = `konvoy ${VERSION}
35
35
  ${formatCommandList()}
36
36
 
37
37
  agents: ${agentIds.join(', ')}
38
- flags: --session <slug>
38
+ flags: --session <slug>, --version (-v), --help (-h)
39
39
  `
40
40
 
41
41
  interface CommandContext {
@@ -115,24 +115,28 @@ 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()
130
122
  const slug = typeof args.flags.session === 'string' ? args.flags.session : undefined
131
123
 
132
- if (command === 'help' || args.flags.help) {
124
+ if (args.flags.version === true || args.flags.v === true || args.flags.V === true) {
125
+ console.log(`konvoy ${VERSION}`)
126
+ return 0
127
+ }
128
+ if (command === 'help' || args.flags.help === true || args.flags.h === true) {
133
129
  console.log(USAGE)
134
130
  return 0
135
131
  }
132
+ if (!command) {
133
+ const stray = Object.keys(args.flags).filter((f) => f !== 'session')
134
+ if (stray.length > 0) {
135
+ console.error(`unknown flag ${stray[0]!.length === 1 ? '-' : '--'}${stray[0]}`)
136
+ console.log(USAGE)
137
+ return 2
138
+ }
139
+ }
136
140
 
137
141
  try {
138
142
  return command ? await dispatch(command, rest, cwd, slug, args) : await interactive(io, cwd, slug)
@@ -160,7 +164,7 @@ async function dispatch(command: string, rest: string[], cwd: string, slug: stri
160
164
  return handlers[name]({ db, cfg, cwd, args, slug }, rest)
161
165
  }
162
166
 
163
- 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> {
164
168
  const db = openDb(dbPath())
165
169
  const cfg = await loadConfig({ cwd: (slug ? getSessionBySlug(db, slug)?.cwd : undefined) ?? cwd })
166
170
  const session = await startSession(db, cfg, cwd, slug)
@@ -168,16 +172,21 @@ async function interactive(io: ReplIo, cwd: string, slug: string | undefined): P
168
172
  console.error(slug ? `no konvoy session named "${slug}"` : 'could not create a session here')
169
173
  return 2
170
174
  }
171
- return runRepl(io, db, cfg, cwd, session, (tokens, current) => {
172
- const a = parseArgs(tokens)
173
- const [cmd = '', ...r] = a._
174
- const name = resolveCommandName(cmd)
175
- if (!name) {
176
- console.error(`unknown command "/${cmd}" - /help lists them`)
177
- return 2
178
- }
179
- return handlers[name]({ db, cfg, cwd, args: a, slug: current }, r)
180
- })
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
+ }
181
190
  }
182
191
 
183
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) {