@doguyilmaz/konvoy 0.1.2 → 0.3.0
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 +20 -0
- package/package.json +1 -1
- package/src/cli.ts +44 -5
- package/src/commands/new.ts +9 -6
- package/src/commands/rename.ts +49 -0
- package/src/commands/repl.ts +125 -0
- package/src/commands/roster.ts +1 -1
- package/src/commands/table.ts +1 -0
- package/src/core/prelude.ts +1 -1
- package/src/paths.ts +2 -0
- package/src/store/queries.ts +8 -0
package/README.md
CHANGED
|
@@ -40,7 +40,9 @@ 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"
|
|
45
|
+
konvoy new # no goal: named after the directory, like sinkaf-8f3a
|
|
44
46
|
konvoy send codex "start with the token refresh path"
|
|
45
47
|
konvoy ls
|
|
46
48
|
konvoy resume # make a session current again and show its roster
|
|
@@ -52,10 +54,25 @@ konvoy attach kiro --id cli_8a1… # adopt a session you started in kiro's own
|
|
|
52
54
|
konvoy doctor
|
|
53
55
|
konvoy update --all # every agent CLI; konvoy itself follows its install channel (see Install)
|
|
54
56
|
konvoy rm stale-slug --yes
|
|
57
|
+
konvoy rename stale-slug token-refresh # the session's .konvoy folder follows
|
|
55
58
|
konvoy version
|
|
56
59
|
konvoy dashboard --port 4000 # local page with the same numbers as `usage --chart`
|
|
57
60
|
```
|
|
58
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
|
+
|
|
59
76
|
## Sample output
|
|
60
77
|
|
|
61
78
|
Captured by running konvoy against a scratch database, not copied from a real project.
|
|
@@ -164,6 +181,9 @@ Global `~/.config/konvoy/config.jsonc`, per project `.konvoy/config.jsonc`. The
|
|
|
164
181
|
file wins. `konvoy config get` shows each agent's resolved settings and whether a value came
|
|
165
182
|
from that agent, from `defaults`, or from konvoy's own built-in.
|
|
166
183
|
|
|
184
|
+
`konvoy new` also writes `.konvoy/.gitignore` (`*`, then `!config.jsonc`), so a session's `CONTEXT.md`
|
|
185
|
+
and `LEDGER.md` never reach git while the project config can be committed.
|
|
186
|
+
|
|
167
187
|
```jsonc
|
|
168
188
|
{
|
|
169
189
|
"defaults": { "effort": "high", "permission": "edit" },
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -18,9 +18,11 @@ import { cmdUpdate } from './commands/update'
|
|
|
18
18
|
import { cmdConfig } from './commands/config'
|
|
19
19
|
import { cmdResume } from './commands/resume'
|
|
20
20
|
import { cmdRm } from './commands/rm'
|
|
21
|
+
import { cmdRename } from './commands/rename'
|
|
21
22
|
import { cmdUsage } from './commands/usage'
|
|
22
23
|
import { cmdDashboard } from './commands/dashboard'
|
|
23
24
|
import { formatCommandList, resolveCommandName, type CommandName } from './commands/table'
|
|
25
|
+
import { runRepl, startSession, type ReplIo } from './commands/repl'
|
|
24
26
|
import type { AgentId } from './types'
|
|
25
27
|
import pkg from '../package.json'
|
|
26
28
|
|
|
@@ -28,6 +30,8 @@ const VERSION = pkg.version
|
|
|
28
30
|
|
|
29
31
|
export const USAGE = `konvoy ${VERSION}
|
|
30
32
|
|
|
33
|
+
konvoy start: resume this directory's session or create one, then talk
|
|
34
|
+
|
|
31
35
|
${formatCommandList()}
|
|
32
36
|
|
|
33
37
|
agents: ${agentIds.join(', ')}
|
|
@@ -78,6 +82,14 @@ const handlers: Record<CommandName, Handler> = {
|
|
|
78
82
|
const [action, key, value] = rest
|
|
79
83
|
return cmdConfig(ctx.cfg, ctx.cwd, action ?? 'get', key, value, { global: ctx.args.flags.global === true })
|
|
80
84
|
},
|
|
85
|
+
rename: (ctx, rest) => {
|
|
86
|
+
const [from, to] = rest
|
|
87
|
+
if (!from || !to) {
|
|
88
|
+
console.error('usage: konvoy rename <session> <new-name>')
|
|
89
|
+
return 2
|
|
90
|
+
}
|
|
91
|
+
return cmdRename(ctx.db, from, to)
|
|
92
|
+
},
|
|
81
93
|
rm: (ctx, rest) => {
|
|
82
94
|
const [target] = rest
|
|
83
95
|
if (!target) {
|
|
@@ -103,20 +115,27 @@ const handlers: Record<CommandName, Handler> = {
|
|
|
103
115
|
}),
|
|
104
116
|
}
|
|
105
117
|
|
|
106
|
-
|
|
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> {
|
|
107
127
|
const args = parseArgs(argv)
|
|
108
128
|
const [command, ...rest] = args._
|
|
109
129
|
const cwd = process.cwd()
|
|
110
130
|
const slug = typeof args.flags.session === 'string' ? args.flags.session : undefined
|
|
111
131
|
|
|
112
|
-
if (
|
|
132
|
+
if (command === 'help' || args.flags.help) {
|
|
113
133
|
console.log(USAGE)
|
|
114
|
-
|
|
115
|
-
return command || args.flags.help ? 0 : 1
|
|
134
|
+
return 0
|
|
116
135
|
}
|
|
117
136
|
|
|
118
137
|
try {
|
|
119
|
-
return await dispatch(command, rest, cwd, slug, args)
|
|
138
|
+
return command ? await dispatch(command, rest, cwd, slug, args) : await interactive(io, cwd, slug)
|
|
120
139
|
} catch (error) {
|
|
121
140
|
if (Bun.env.KONVOY_DEBUG === '1') throw error
|
|
122
141
|
console.error(`konvoy: ${error instanceof Error ? error.message : String(error)}`)
|
|
@@ -141,6 +160,26 @@ async function dispatch(command: string, rest: string[], cwd: string, slug: stri
|
|
|
141
160
|
return handlers[name]({ db, cfg, cwd, args, slug }, rest)
|
|
142
161
|
}
|
|
143
162
|
|
|
163
|
+
async function interactive(io: ReplIo, cwd: string, slug: string | undefined): Promise<number> {
|
|
164
|
+
const db = openDb(dbPath())
|
|
165
|
+
const cfg = await loadConfig({ cwd: (slug ? getSessionBySlug(db, slug)?.cwd : undefined) ?? cwd })
|
|
166
|
+
const session = await startSession(db, cfg, cwd, slug)
|
|
167
|
+
if (!session) {
|
|
168
|
+
console.error(slug ? `no konvoy session named "${slug}"` : 'could not create a session here')
|
|
169
|
+
return 2
|
|
170
|
+
}
|
|
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
|
+
})
|
|
181
|
+
}
|
|
182
|
+
|
|
144
183
|
if (import.meta.main) {
|
|
145
184
|
process.exitCode = await main(Bun.argv.slice(2))
|
|
146
185
|
}
|
package/src/commands/new.ts
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
import type { Database } from 'bun:sqlite'
|
|
2
2
|
import type { Config } from '../config/schema'
|
|
3
|
-
import { newSession } from '../core/session'
|
|
4
|
-
import { sessionDir } from '../paths'
|
|
3
|
+
import { newSession, slugify } from '../core/session'
|
|
4
|
+
import { basename, join, sessionDir } from '../paths'
|
|
5
5
|
|
|
6
6
|
export async function cmdNew(db: Database, cfg: Config, cwd: string, goal: string): Promise<number> {
|
|
7
7
|
const lead = cfg.roles.lead ?? 'claude'
|
|
8
|
-
const
|
|
8
|
+
const suffix = Bun.randomUUIDv7().slice(-4)
|
|
9
|
+
const slug = goal ? undefined : `${slugify(basename(cwd)).slice(0, 30)}-${suffix}`
|
|
10
|
+
const session = newSession(db, { cwd, goal, lead, slug })
|
|
9
11
|
const dir = sessionDir(cwd, session.slug)
|
|
12
|
+
const ignore = Bun.file(join(cwd, '.konvoy', '.gitignore'))
|
|
13
|
+
if (!(await ignore.exists())) await Bun.write(ignore, '*\n!config.jsonc\n')
|
|
10
14
|
// `konvoy rm` frees the slug but leaves these files - they are the user's. A new session under
|
|
11
15
|
// an old slug adds its goal to the context and appends to the ledger; it truncates neither.
|
|
12
16
|
const context = Bun.file(`${dir}/CONTEXT.md`)
|
|
17
|
+
const goalSection = session.goal ? `\n## Goal\n\n${session.goal}\n` : ''
|
|
13
18
|
await Bun.write(
|
|
14
19
|
context,
|
|
15
|
-
(await context.exists())
|
|
16
|
-
? `${await context.text()}\n## Goal\n\n${session.goal}\n`
|
|
17
|
-
: `# ${session.slug}\n\n## Goal\n\n${session.goal}\n`,
|
|
20
|
+
(await context.exists()) ? `${await context.text()}${goalSection}` : `# ${session.slug}\n${goalSection}`,
|
|
18
21
|
)
|
|
19
22
|
const ledger = Bun.file(`${dir}/LEDGER.md`)
|
|
20
23
|
if (!(await ledger.exists())) await Bun.write(ledger, `# Ledger - ${session.slug}\n`)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Database } from 'bun:sqlite'
|
|
2
|
+
import { slugify } from '../core/session'
|
|
3
|
+
import { sessionDir } from '../paths'
|
|
4
|
+
import { getSessionBySlug, lockOwner, renameSession } from '../store/queries'
|
|
5
|
+
|
|
6
|
+
const isDir = async (path: string): Promise<boolean> => (await Bun.$`test -d ${path}`.quiet().nothrow()).exitCode === 0
|
|
7
|
+
|
|
8
|
+
async function retitle(path: string, from: string, to: string): Promise<void> {
|
|
9
|
+
const file = Bun.file(path)
|
|
10
|
+
if (!(await file.exists())) return
|
|
11
|
+
const text = await file.text()
|
|
12
|
+
if (text.startsWith(from)) await Bun.write(file, to + text.slice(from.length))
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function cmdRename(db: Database, from: string, to: string): Promise<number> {
|
|
16
|
+
const session = getSessionBySlug(db, from)
|
|
17
|
+
if (!session) {
|
|
18
|
+
console.error(`no konvoy session named "${from}"`)
|
|
19
|
+
return 2
|
|
20
|
+
}
|
|
21
|
+
const busy = lockOwner(db, session.id)
|
|
22
|
+
if (busy) {
|
|
23
|
+
console.error(`"${from}" has a turn running (${busy}) - wait for it to finish, then retry`)
|
|
24
|
+
return 2
|
|
25
|
+
}
|
|
26
|
+
const slug = slugify(to)
|
|
27
|
+
if (getSessionBySlug(db, slug)) {
|
|
28
|
+
console.error(`a session named "${slug}" already exists`)
|
|
29
|
+
return 2
|
|
30
|
+
}
|
|
31
|
+
const oldDir = sessionDir(session.cwd, from)
|
|
32
|
+
const newDir = sessionDir(session.cwd, slug)
|
|
33
|
+
if (await isDir(oldDir)) {
|
|
34
|
+
if (await isDir(newDir)) {
|
|
35
|
+
console.error(`${newDir} already exists - move it away first`)
|
|
36
|
+
return 2
|
|
37
|
+
}
|
|
38
|
+
const moved = await Bun.$`mv ${oldDir} ${newDir}`.quiet().nothrow()
|
|
39
|
+
if (moved.exitCode !== 0) {
|
|
40
|
+
console.error(`could not move ${oldDir} to ${newDir}: ${moved.stderr.toString().trim()}`)
|
|
41
|
+
return 1
|
|
42
|
+
}
|
|
43
|
+
await retitle(`${newDir}/CONTEXT.md`, `# ${from}\n`, `# ${slug}\n`)
|
|
44
|
+
await retitle(`${newDir}/LEDGER.md`, `# Ledger - ${from}\n`, `# Ledger - ${slug}\n`)
|
|
45
|
+
}
|
|
46
|
+
renameSession(db, session.id, slug)
|
|
47
|
+
console.log(`renamed ${from} -> ${slug}`)
|
|
48
|
+
return 0
|
|
49
|
+
}
|
|
@@ -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/commands/table.ts
CHANGED
|
@@ -21,6 +21,7 @@ export const commandTable = [
|
|
|
21
21
|
usage: 'rm <session> --yes',
|
|
22
22
|
summary: 'delete a konvoy session (foreign sessions survive)',
|
|
23
23
|
},
|
|
24
|
+
{ name: 'rename', aliases: [], usage: 'rename <session> <new-name>', summary: 'rename a session; its .konvoy folder follows' },
|
|
24
25
|
{ name: 'roster', aliases: [], usage: 'roster', summary: 'who is in the convoy' },
|
|
25
26
|
{ name: 'usage', aliases: [], usage: 'usage [--all] [--chart]', summary: 'what this session spent, per agent' },
|
|
26
27
|
{ name: 'status', aliases: [], usage: 'status', summary: 'versions, auth and roster' },
|
package/src/core/prelude.ts
CHANGED
|
@@ -117,5 +117,5 @@ export function buildPrelude(db: Database, session: Session, facts: string, opts
|
|
|
117
117
|
|
|
118
118
|
if (dropped > 0) turnBlocks.push(`(${dropped} earlier turn${dropped === 1 ? '' : 's'} not shown)`)
|
|
119
119
|
|
|
120
|
-
return [`goal: ${session.goal}
|
|
120
|
+
return [session.goal ? `goal: ${session.goal}` : '', facts, turnBlocks.join('\n\n')].filter(Boolean).join('\n\n')
|
|
121
121
|
}
|
package/src/paths.ts
CHANGED
|
@@ -14,6 +14,8 @@ export function join(...parts: string[]): string {
|
|
|
14
14
|
return (absolute ? '/' : '') + segments.join('/')
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
export const basename = (p: string): string => p.split('/').filter(Boolean).at(-1) ?? ''
|
|
18
|
+
|
|
17
19
|
export function dirname(p: string): string {
|
|
18
20
|
const cut = p.lastIndexOf('/')
|
|
19
21
|
if (cut < 0) return '.'
|
package/src/store/queries.ts
CHANGED
|
@@ -76,6 +76,14 @@ 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
|
+
|
|
83
|
+
export function renameSession(db: Database, id: string, slug: string): void {
|
|
84
|
+
db.query('UPDATE session SET slug = $slug WHERE id = $id').run({ slug, id })
|
|
85
|
+
}
|
|
86
|
+
|
|
79
87
|
export function deleteSession(db: Database, id: string): void {
|
|
80
88
|
db.transaction(() => {
|
|
81
89
|
db.query('DELETE FROM event WHERE turn_id IN (SELECT id FROM turn WHERE session_id = $id)').run({ id })
|