@doguyilmaz/konvoy 0.1.2 → 0.2.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 CHANGED
@@ -41,6 +41,7 @@ to download; the npm package is a few kilobytes of source and runs on the Bun yo
41
41
 
42
42
  ```bash
43
43
  konvoy new "refactor the auth layer"
44
+ konvoy new # no goal: named after the directory, like sinkaf-8f3a
44
45
  konvoy send codex "start with the token refresh path"
45
46
  konvoy ls
46
47
  konvoy resume # make a session current again and show its roster
@@ -52,6 +53,7 @@ konvoy attach kiro --id cli_8a1… # adopt a session you started in kiro's own
52
53
  konvoy doctor
53
54
  konvoy update --all # every agent CLI; konvoy itself follows its install channel (see Install)
54
55
  konvoy rm stale-slug --yes
56
+ konvoy rename stale-slug token-refresh # the session's .konvoy folder follows
55
57
  konvoy version
56
58
  konvoy dashboard --port 4000 # local page with the same numbers as `usage --chart`
57
59
  ```
@@ -164,6 +166,9 @@ Global `~/.config/konvoy/config.jsonc`, per project `.konvoy/config.jsonc`. The
164
166
  file wins. `konvoy config get` shows each agent's resolved settings and whether a value came
165
167
  from that agent, from `defaults`, or from konvoy's own built-in.
166
168
 
169
+ `konvoy new` also writes `.konvoy/.gitignore` (`*`, then `!config.jsonc`), so a session's `CONTEXT.md`
170
+ and `LEDGER.md` never reach git while the project config can be committed.
171
+
167
172
  ```jsonc
168
173
  {
169
174
  "defaults": { "effort": "high", "permission": "edit" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doguyilmaz/konvoy",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
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
@@ -18,6 +18,7 @@ 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'
@@ -78,6 +79,14 @@ const handlers: Record<CommandName, Handler> = {
78
79
  const [action, key, value] = rest
79
80
  return cmdConfig(ctx.cfg, ctx.cwd, action ?? 'get', key, value, { global: ctx.args.flags.global === true })
80
81
  },
82
+ rename: (ctx, rest) => {
83
+ const [from, to] = rest
84
+ if (!from || !to) {
85
+ console.error('usage: konvoy rename <session> <new-name>')
86
+ return 2
87
+ }
88
+ return cmdRename(ctx.db, from, to)
89
+ },
81
90
  rm: (ctx, rest) => {
82
91
  const [target] = rest
83
92
  if (!target) {
@@ -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 session = newSession(db, { cwd, goal: goal || 'untitled', lead })
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
+ }
@@ -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' },
@@ -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}`, facts, turnBlocks.join('\n\n')].filter(Boolean).join('\n\n')
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 '.'
@@ -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 renameSession(db: Database, id: string, slug: string): void {
80
+ db.query('UPDATE session SET slug = $slug WHERE id = $id').run({ slug, id })
81
+ }
82
+
79
83
  export function deleteSession(db: Database, id: string): void {
80
84
  db.transaction(() => {
81
85
  db.query('DELETE FROM event WHERE turn_id IN (SELECT id FROM turn WHERE session_id = $id)').run({ id })