@doguyilmaz/konvoy 0.2.0 → 0.3.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/README.md +15 -0
- package/package.json +1 -1
- package/src/args.ts +1 -1
- package/src/cli.ts +48 -6
- package/src/commands/repl.ts +125 -0
- package/src/commands/roster.ts +1 -1
- package/src/store/queries.ts +4 -0
package/README.md
CHANGED
|
@@ -40,6 +40,7 @@ to download; the npm package is a few kilobytes of source and runs on the Bun yo
|
|
|
40
40
|
## Use
|
|
41
41
|
|
|
42
42
|
```bash
|
|
43
|
+
konvoy # start: resume this directory's session or create one, then talk
|
|
43
44
|
konvoy new "refactor the auth layer"
|
|
44
45
|
konvoy new # no goal: named after the directory, like sinkaf-8f3a
|
|
45
46
|
konvoy send codex "start with the token refresh path"
|
|
@@ -58,6 +59,20 @@ konvoy version
|
|
|
58
59
|
konvoy dashboard --port 4000 # local page with the same numbers as `usage --chart`
|
|
59
60
|
```
|
|
60
61
|
|
|
62
|
+
Bare `konvoy` is the everyday entry: it resumes the session bound to this directory or creates
|
|
63
|
+
one named after it, then reads what you type. Plain text is a turn against the current agent; a
|
|
64
|
+
line starting with `/` runs any command from the list (`/usage --all`, `/rename token-refresh`,
|
|
65
|
+
`/attach`), plus `/use <agent>`, `/goal <text>`, `/help` and `/quit`. Ctrl-D leaves, Ctrl-C stops a
|
|
66
|
+
running turn and leaves, and piped stdin runs one turn per line.
|
|
67
|
+
|
|
68
|
+
```text
|
|
69
|
+
sinkaf-8f3a claude> fix the token refresh
|
|
70
|
+
· Read src/auth.ts
|
|
71
|
+
Switched the refresh to fire on 401 with a single in-flight retry.
|
|
72
|
+
sinkaf-8f3a claude> /use codex
|
|
73
|
+
sinkaf-8f3a codex> review that diff
|
|
74
|
+
```
|
|
75
|
+
|
|
61
76
|
## Sample output
|
|
62
77
|
|
|
63
78
|
Captured by running konvoy against a scratch database, not copied from a real project.
|
package/package.json
CHANGED
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,6 +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
26
|
import type { AgentId } from './types'
|
|
26
27
|
import pkg from '../package.json'
|
|
27
28
|
|
|
@@ -29,10 +30,12 @@ const VERSION = pkg.version
|
|
|
29
30
|
|
|
30
31
|
export const USAGE = `konvoy ${VERSION}
|
|
31
32
|
|
|
33
|
+
konvoy start: resume this directory's session or create one, then talk
|
|
34
|
+
|
|
32
35
|
${formatCommandList()}
|
|
33
36
|
|
|
34
37
|
agents: ${agentIds.join(', ')}
|
|
35
|
-
flags: --session <slug
|
|
38
|
+
flags: --session <slug>, --version (-v), --help (-h)
|
|
36
39
|
`
|
|
37
40
|
|
|
38
41
|
interface CommandContext {
|
|
@@ -112,20 +115,39 @@ const handlers: Record<CommandName, Handler> = {
|
|
|
112
115
|
}),
|
|
113
116
|
}
|
|
114
117
|
|
|
115
|
-
|
|
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> {
|
|
116
127
|
const args = parseArgs(argv)
|
|
117
128
|
const [command, ...rest] = args._
|
|
118
129
|
const cwd = process.cwd()
|
|
119
130
|
const slug = typeof args.flags.session === 'string' ? args.flags.session : undefined
|
|
120
131
|
|
|
121
|
-
if (
|
|
132
|
+
if (args.flags.version === true || args.flags.v === true || args.flags.V === true) {
|
|
133
|
+
console.log(`konvoy ${VERSION}`)
|
|
134
|
+
return 0
|
|
135
|
+
}
|
|
136
|
+
if (command === 'help' || args.flags.help === true || args.flags.h === true) {
|
|
122
137
|
console.log(USAGE)
|
|
123
|
-
|
|
124
|
-
|
|
138
|
+
return 0
|
|
139
|
+
}
|
|
140
|
+
if (!command) {
|
|
141
|
+
const stray = Object.keys(args.flags).filter((f) => f !== 'session')
|
|
142
|
+
if (stray.length > 0) {
|
|
143
|
+
console.error(`unknown flag ${stray[0]!.length === 1 ? '-' : '--'}${stray[0]}`)
|
|
144
|
+
console.log(USAGE)
|
|
145
|
+
return 2
|
|
146
|
+
}
|
|
125
147
|
}
|
|
126
148
|
|
|
127
149
|
try {
|
|
128
|
-
return await dispatch(command, rest, cwd, slug, args)
|
|
150
|
+
return command ? await dispatch(command, rest, cwd, slug, args) : await interactive(io, cwd, slug)
|
|
129
151
|
} catch (error) {
|
|
130
152
|
if (Bun.env.KONVOY_DEBUG === '1') throw error
|
|
131
153
|
console.error(`konvoy: ${error instanceof Error ? error.message : String(error)}`)
|
|
@@ -150,6 +172,26 @@ async function dispatch(command: string, rest: string[], cwd: string, slug: stri
|
|
|
150
172
|
return handlers[name]({ db, cfg, cwd, args, slug }, rest)
|
|
151
173
|
}
|
|
152
174
|
|
|
175
|
+
async function interactive(io: ReplIo, cwd: string, slug: string | undefined): Promise<number> {
|
|
176
|
+
const db = openDb(dbPath())
|
|
177
|
+
const cfg = await loadConfig({ cwd: (slug ? getSessionBySlug(db, slug)?.cwd : undefined) ?? cwd })
|
|
178
|
+
const session = await startSession(db, cfg, cwd, slug)
|
|
179
|
+
if (!session) {
|
|
180
|
+
console.error(slug ? `no konvoy session named "${slug}"` : 'could not create a session here')
|
|
181
|
+
return 2
|
|
182
|
+
}
|
|
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
|
+
})
|
|
193
|
+
}
|
|
194
|
+
|
|
153
195
|
if (import.meta.main) {
|
|
154
196
|
process.exitCode = await main(Bun.argv.slice(2))
|
|
155
197
|
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { Database } from 'bun:sqlite'
|
|
2
|
+
import type { Config } from '../config/schema'
|
|
3
|
+
import type { AgentId, Session } from '../types'
|
|
4
|
+
import { agentIds } from '../adapters'
|
|
5
|
+
import { resolveAgent } from '../config/load'
|
|
6
|
+
import { sessionDir } from '../paths'
|
|
7
|
+
import { currentSession, getSessionBySlug, lastTurnAgent, listSessions, setGoal } from '../store/queries'
|
|
8
|
+
import { cmdNew } from './new'
|
|
9
|
+
import { formatCommandList } from './table'
|
|
10
|
+
|
|
11
|
+
export interface ReplIo {
|
|
12
|
+
lines: AsyncIterable<string>
|
|
13
|
+
write: (text: string) => void
|
|
14
|
+
tty: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** runs one command line through the command table, as `konvoy <tokens>` would, for the given session */
|
|
18
|
+
export type Run = (tokens: string[], slug: string) => Promise<number> | number
|
|
19
|
+
|
|
20
|
+
const INNER = ` /use <agent> talk to this agent from now on
|
|
21
|
+
/goal <text> set the session goal
|
|
22
|
+
/help this list
|
|
23
|
+
/quit leave (Ctrl-D does too)
|
|
24
|
+
`
|
|
25
|
+
|
|
26
|
+
export const replHelp = (): string => INNER + formatCommandList().replaceAll(' konvoy ', ' /') + '\n'
|
|
27
|
+
|
|
28
|
+
export async function startSession(db: Database, cfg: Config, cwd: string, slug?: string): Promise<Session | null> {
|
|
29
|
+
if (slug) return getSessionBySlug(db, slug)
|
|
30
|
+
const current = currentSession(db, cwd)
|
|
31
|
+
if (current) return current
|
|
32
|
+
if ((await cmdNew(db, cfg, cwd, '')) !== 0) return null
|
|
33
|
+
return currentSession(db, cwd)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function runRepl(
|
|
37
|
+
io: ReplIo,
|
|
38
|
+
db: Database,
|
|
39
|
+
cfg: Config,
|
|
40
|
+
cwd: string,
|
|
41
|
+
start: Session,
|
|
42
|
+
run: Run,
|
|
43
|
+
): Promise<number> {
|
|
44
|
+
let session = start
|
|
45
|
+
let agent: AgentId = session.lead
|
|
46
|
+
const prompt = (): void => {
|
|
47
|
+
if (io.tty) io.write(`${session.slug} ${agent}> `)
|
|
48
|
+
}
|
|
49
|
+
const refresh = (): boolean => {
|
|
50
|
+
const fresh = listSessions(db).find((s) => s.id === session.id)
|
|
51
|
+
if (fresh) session = fresh
|
|
52
|
+
return fresh !== undefined
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
prompt()
|
|
56
|
+
for await (const raw of io.lines) {
|
|
57
|
+
const line = raw.trim()
|
|
58
|
+
if (line === '') {
|
|
59
|
+
prompt()
|
|
60
|
+
continue
|
|
61
|
+
}
|
|
62
|
+
if (!line.startsWith('/')) {
|
|
63
|
+
await run(['send', agent, line], session.slug)
|
|
64
|
+
// failover never falls back, so the agent that answered is the one to keep talking to
|
|
65
|
+
const moved = lastTurnAgent(db, session.id)
|
|
66
|
+
if (moved && moved !== agent) agent = moved
|
|
67
|
+
prompt()
|
|
68
|
+
continue
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const [cmd = '', ...rest] = line.slice(1).split(/\s+/)
|
|
72
|
+
if (cmd === 'quit' || cmd === 'exit' || cmd === 'q') break
|
|
73
|
+
if (cmd === 'help') {
|
|
74
|
+
io.write(replHelp())
|
|
75
|
+
} else if (cmd === 'use') {
|
|
76
|
+
const next = rest[0] ?? ''
|
|
77
|
+
if (!agentIds.includes(next as AgentId)) {
|
|
78
|
+
console.error(`unknown agent "${next}" - expected one of ${agentIds.join(', ')}`)
|
|
79
|
+
} else if (!resolveAgent(cfg, next as AgentId).enabled) {
|
|
80
|
+
console.error(`${next} is disabled in this konvoy config`)
|
|
81
|
+
} else {
|
|
82
|
+
agent = next as AgentId
|
|
83
|
+
}
|
|
84
|
+
} else if (cmd === 'goal') {
|
|
85
|
+
const goal = rest.join(' ')
|
|
86
|
+
if (!goal) {
|
|
87
|
+
console.error('usage: /goal <text>')
|
|
88
|
+
} else {
|
|
89
|
+
setGoal(db, session.id, goal)
|
|
90
|
+
const context = Bun.file(`${sessionDir(session.cwd, session.slug)}/CONTEXT.md`)
|
|
91
|
+
if (await context.exists()) await Bun.write(context, `${await context.text()}\n## Goal\n\n${goal}\n`)
|
|
92
|
+
refresh()
|
|
93
|
+
}
|
|
94
|
+
} else if (cmd === 'rename') {
|
|
95
|
+
if (rest.length === 0) console.error('usage: /rename <new-name>')
|
|
96
|
+
else {
|
|
97
|
+
await run(['rename', session.slug, rest.join(' ')], session.slug)
|
|
98
|
+
refresh()
|
|
99
|
+
}
|
|
100
|
+
} else if (cmd === 'attach') {
|
|
101
|
+
await run(['attach', rest[0] ?? agent, ...rest.slice(1)], session.slug)
|
|
102
|
+
} else {
|
|
103
|
+
await run([cmd, ...rest], session.slug)
|
|
104
|
+
if (cmd === 'resume' && rest[0]) {
|
|
105
|
+
const target = getSessionBySlug(db, rest[0])
|
|
106
|
+
if (target) {
|
|
107
|
+
session = target
|
|
108
|
+
agent = target.lead
|
|
109
|
+
}
|
|
110
|
+
} else if (cmd === 'new') {
|
|
111
|
+
const created = currentSession(db, cwd)
|
|
112
|
+
if (created) {
|
|
113
|
+
session = created
|
|
114
|
+
agent = created.lead
|
|
115
|
+
}
|
|
116
|
+
} else if (!refresh()) {
|
|
117
|
+
console.error(`session ${session.slug} is gone`)
|
|
118
|
+
return 0
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
prompt()
|
|
122
|
+
}
|
|
123
|
+
if (io.tty) io.write('\n')
|
|
124
|
+
return 0
|
|
125
|
+
}
|
package/src/commands/roster.ts
CHANGED
|
@@ -28,7 +28,7 @@ export function cmdRoster(db: Database, cfg: Config, cwd: string, slug?: string)
|
|
|
28
28
|
}
|
|
29
29
|
})
|
|
30
30
|
|
|
31
|
-
console.log(`session ${session.slug} - ${session.goal}`)
|
|
31
|
+
console.log(session.goal ? `session ${session.slug} - ${session.goal}` : `session ${session.slug}`)
|
|
32
32
|
console.log(formatRoster(rows))
|
|
33
33
|
for (const model of duplicateModels(rows)) {
|
|
34
34
|
console.log(`warning: ${model} is used by more than one agent - a second opinion from the same model is not one`)
|
package/src/store/queries.ts
CHANGED
|
@@ -76,6 +76,10 @@ export function touchSession(db: Database, id: string): void {
|
|
|
76
76
|
).run({ id, now: now() })
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
export function setGoal(db: Database, id: string, goal: string): void {
|
|
80
|
+
db.query('UPDATE session SET goal = $goal WHERE id = $id').run({ goal, id })
|
|
81
|
+
}
|
|
82
|
+
|
|
79
83
|
export function renameSession(db: Database, id: string, slug: string): void {
|
|
80
84
|
db.query('UPDATE session SET slug = $slug WHERE id = $id').run({ slug, id })
|
|
81
85
|
}
|