opencode-sesh 0.1.3 → 0.1.4

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/AGENTS.md CHANGED
@@ -13,8 +13,8 @@ territory: read this first, then the specific file you need.
13
13
  2. **`tui/sesh-panel.tsx`** — a SolidJS/OpenTUI plugin for the opencode TUI: a
14
14
  recent-sessions section in the sidebar plus a `ctrl+o` / `/sesh` picker.
15
15
  3. **`opencode/tools/sesh-list.ts`** — the `sesh-list` custom tool, so the agent
16
- can list sessions across every directory (the native `opencode session list`
17
- is project-scoped).
16
+ can list sessions across every directory by querying the global `opencode
17
+ db` store (the native `opencode session list` is project-scoped).
18
18
 
19
19
  No iTerm2, no AppleScript, no panes, no window management — **never add any.**
20
20
  opencode has no persistent-sidebar API, so the "sidebar" is a `sidebar_content`
@@ -43,9 +43,10 @@ The **filename becomes the tool name** in opencode, so `sesh-list.ts` is the
43
43
 
44
44
  ## Data flow (fzf picker)
45
45
 
46
- `bin/sesh-list.sh` is the only place that knows the session store (opencode
47
- SQLite DB: `session` / `message` / `part` tables). Keystrokes never touch the
48
- database:
46
+ `bin/sesh-list.sh` owns the fzf picker's session store access (opencode SQLite
47
+ DB: `session` / `message` / `part` tables); the `sesh-list` agent tool queries
48
+ the same store through `opencode db`. Keep its SQL column names in sync with
49
+ the schema below. Keystrokes never touch the database:
49
50
 
50
51
  - `--refresh` (expensive): bulk-reads session metadata and per-session text
51
52
  parts, validates the persistent extraction cache
@@ -67,7 +68,7 @@ One worker (`sesh-refresh-worker.sh`, per picker, TERMed on exit) serializes
67
68
  scans; fzf's `start`/`every(3)`/`change` bindings render only. Selection requests
68
69
  a fresh poll (`request`/`wait`, ≤12 s) before dispatch. opencode has no
69
70
  live-agent API, so `liveState` is always `unknown` and rows render gray;
70
- dispatch is always `opencode --session <id>` (`--fork` with Ctrl-F/Enter).
71
+ dispatch is always `opencode --session <id>` (`--fork` with Ctrl-F or --fork).
71
72
  Resume happens in place (`exec` after `cd` to the session's cwd).
72
73
 
73
74
  ## Conventions
@@ -82,12 +83,16 @@ Resume happens in place (`exec` after `cd` to the session's cwd).
82
83
  `sqlite3 -json` in `-R` mode; it pretty-prints — parse as JSON.
83
84
  - `jq -R` output stays JSON-quoted: use `-Rr` when a shell variable needs the
84
85
  raw string.
85
- - fzf: stock only, `>= 0.52` (`transform:`, `every():`). `{q}` in binds is
86
+ - fzf: stock only, `>= 0.73` (`transform:`, `every():`). `{q}` in binds is
86
87
  shell-quoted by fzf — don't add quoting. Ctrl-G is bind-only (stays open);
87
88
  Ctrl-F is `--expect` (accepts with fork).
88
89
  - The installer prefers the local opencode version for `@opencode-ai/*` and
89
90
  merges (never clobbers) the config `package.json`; opencode runs `bun install`
90
91
  at startup.
92
+ - Commit messages follow [Conventional Commits](https://www.conventionalcommits.org/)
93
+ (`feat`, `fix`, `docs`, `ci`, …). See
94
+ [CONTRIBUTING.md](CONTRIBUTING.md#commit-messages) for the types and the
95
+ version mapping.
91
96
 
92
97
  ## TUI panel (`tui/sesh-panel.tsx`)
93
98
 
package/CHANGELOG.md CHANGED
@@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.4] - 2026-09-13
11
+
12
+ ### Fixed
13
+
14
+ - The picker keeps the selected session across automatic refreshes and no
15
+ longer retargets Enter, Ctrl-F, or Ctrl-X when the selected session
16
+ disappears (`--track`/`--id-nth` plus a non-actionable placeholder row).
17
+ - Ctrl-F now forks: fzf's `--print-query`/`--expect` output (query, key, row)
18
+ was parsed as key, query, row, so forking resumed the original session.
19
+ - Require fzf >= 0.73 in the launcher, installer, README, and AGENTS — the
20
+ `every()` refresh event the picker relies on was added in 0.73.
21
+ - Transcript extraction caches are written 0700/0600 regardless of the caller's
22
+ umask, and existing caches are tightened on refresh.
23
+
24
+ ### Changed
25
+
26
+ - The `sesh-list` agent tool queries the global session store, so it lists
27
+ every project directory (roots, non-archived) instead of only the current
28
+ project; directory filtering now happens before the limit.
29
+
30
+ ### Added
31
+
32
+ - Real-fzf PTY regressions for selection tracking and fork parsing
33
+ (`npm run test:picker`), an agent-tool contract/SQL suite, and fzf
34
+ version-gate and cache-permission regressions.
35
+
10
36
  ## [0.1.3] - 2026-09-12
11
37
 
12
38
  ### Added
@@ -48,7 +74,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
48
74
  - Fixture-database regression suite, TypeScript typechecking, and ShellCheck in
49
75
  CI.
50
76
 
51
- [Unreleased]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.1.3...HEAD
77
+ [Unreleased]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.1.4...HEAD
78
+ [0.1.4]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.1.3...v0.1.4
52
79
  [0.1.3]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.1.2...v0.1.3
53
80
  [0.1.2]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.1.1...v0.1.2
54
81
  [0.1.1]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.1.0...v0.1.1
package/README.md CHANGED
@@ -82,7 +82,7 @@ ctrl+o # inside opencode — picker
82
82
  ### Requirements
83
83
 
84
84
  - [opencode](https://opencode.ai), used at least once
85
- - `bash`, `jq`, and [`fzf`](https://github.com/junegunn/fzf) `>= 0.52`
85
+ - `bash`, `jq`, and [`fzf`](https://github.com/junegunn/fzf) `>= 0.73`
86
86
  - `sqlite3` recommended — the picker queries the session DB directly in
87
87
  milliseconds and falls back to the slower `opencode db` CLI without it
88
88
  - [`glow`](https://github.com/charmbracelet/glow) optional — styles the
@@ -104,7 +104,7 @@ bash install.sh --uninstall # from source
104
104
  | --- | --- |
105
105
  | Type | Search titles and transcript text across all directories |
106
106
  | Enter | Resume the selected session in this terminal |
107
- | Ctrl-F Enter | Resume as a fork (the original is untouched) |
107
+ | Ctrl-F | Resume as a fork (the original is untouched) |
108
108
  | Ctrl-G | Toggle current-directory scope / all sessions |
109
109
  | Space (empty query) | Toggle the transcript preview |
110
110
  | `?` (empty query) | Toggle shortcut help |
package/bin/sesh-list.sh CHANGED
@@ -6,6 +6,7 @@
6
6
  # TSV: agentId, sessionId, liveState, display, cwd, trackingId.
7
7
  # Every row, including headers/notices, requires a unique nonempty trackingId.
8
8
  set -euo pipefail
9
+ umask 077
9
10
 
10
11
  LIMIT=0
11
12
  LIMIT_SET=0
@@ -17,6 +18,7 @@ INCLUDE_ARCHIVED=0
17
18
  ARCHIVED_SET=0
18
19
  STATE_DIR=''
19
20
  QUERY=''
21
+ SELECTED_ID=''
20
22
  SCOPE_CWD=''
21
23
  usage() { echo "usage: ${0##*/} [--refresh] [--wait-lock SECONDS] --state-dir DIR [--cwd] [--limit N] [--archived] [--toggle-scope] [--query TEXT]" >&2; }
22
24
  while [ "$#" -gt 0 ]; do
@@ -29,6 +31,7 @@ while [ "$#" -gt 0 ]; do
29
31
  --archived) INCLUDE_ARCHIVED=1; ARCHIVED_SET=1 ;;
30
32
  --toggle-scope) TOGGLE_SCOPE=1 ;;
31
33
  --query) QUERY=${2:-}; shift ;;
34
+ --selected-id) SELECTED_ID=${2:-}; shift ;;
32
35
  --help) usage; exit 0 ;;
33
36
  *) echo "unknown option: $1" >&2; usage; exit 2 ;;
34
37
  esac
@@ -111,18 +114,24 @@ set_status() { atomic_text "$STATUS" "$1"; }
111
114
  render() {
112
115
  local state=''
113
116
  [ -f "$STATUS" ] && state=$(cat "$STATUS" 2>/dev/null || true)
114
- if [ ! -s "$SNAPSHOT" ]; then
117
+ if [ ! -f "$SNAPSHOT" ]; then
115
118
  [ -n "$state" ] && [ "$state" != fresh ] && printf '\t\tunknown\t%s%s%s\t\tnotice:live-state\n' "$YELLOW" "$state" "$RESET"
116
119
  return 0
117
120
  fi
118
121
  {
119
122
  [ -n "$state" ] && [ "$state" != fresh ] && printf '\t\tunknown\t%s%s%s\t\tnotice:live-state\n' "$YELLOW" "$state" "$RESET"
120
- "$JQ_BIN" -sr --arg green "$GREEN" --arg cyan "$CYAN" --arg gray "$GRAY" --arg magenta "$MAGENTA" --arg red "$RED" --arg bold "$BOLD" --arg reset "$RESET" --arg query "$QUERY" --arg home "$HOME" '
123
+ "$JQ_BIN" -sr --arg green "$GREEN" --arg cyan "$CYAN" --arg gray "$GRAY" --arg magenta "$MAGENTA" --arg red "$RED" --arg bold "$BOLD" --arg yellow "$YELLOW" --arg reset "$RESET" --arg query "$QUERY" --arg home "$HOME" --arg selected "$SELECTED_ID" '
121
124
  def ago($epoch):
122
125
  ((now - ($epoch | tonumber)) | floor) as $s
123
126
  | if $s < 60 then "now" elif $s < 3600 then "\($s / 60 | floor)m" elif $s < 86400 then "\($s / 3600 | floor)h" else "\($s / 86400 | floor)d" end;
124
127
  def homepath:
125
128
  . as $p | if $p == "" then "?" elif $p == $home then "~" elif ($home != "" and startswith($home + "/")) then "~" + .[($home|length):] else . end;
129
+ # Preserve a vanished selection as a non-actionable row with its original
130
+ # tracking key. Otherwise fzf falls back to a different session. Keep it
131
+ # until the user moves away; empty field 2 makes Enter/Ctrl-F/Ctrl-X no-ops.
132
+ (if ($selected | test("^ses_[A-Za-z0-9]+$")) and (any(.sessionId == $selected) | not)
133
+ then (["", "", "unknown", ($yellow + "Selected session is no longer available; choose another session" + $reset), "", $selected] | @tsv)
134
+ else empty end),
126
135
  (($query | ascii_downcase) as $q
127
136
  | map(select($q == "" or (.searchText | contains($q))))
128
137
  | group_by(.cwd)
@@ -159,8 +168,22 @@ acquire_lock_wait() {
159
168
  done
160
169
  }
161
170
 
171
+ private_dir() {
172
+ # These are dedicated sesh directories, never the shared XDG parent. Refuse
173
+ # symlinks/foreign ownership rather than changing permissions on their target.
174
+ [ ! -L "$1" ] || { echo "sesh: private directory is a symlink: $1" >&2; return 1; }
175
+ mkdir -p "$1"
176
+ [ -O "$1" ] || { echo "sesh: private directory is not owned by this user: $1" >&2; return 1; }
177
+ chmod 700 "$1"
178
+ }
179
+
162
180
  refresh() {
163
- mkdir -p "$STATE_DIR"
181
+ private_dir "$STATE_DIR"
182
+ private_dir "$CACHE_DIR"
183
+ private_dir "$EXTRACTIONS"
184
+ # Migrate warm caches too, including temporary files left by older versions.
185
+ # Do not follow symlinks or chmod unrelated files outside our extraction store.
186
+ find "$EXTRACTIONS" -maxdepth 1 -type f -user "$(id -u)" \( -name 'ses_*.json' -o -name 'ses_*.json.tmp' \) -exec chmod 600 {} +
164
187
  if [ "${SESH_LOCK_HELD:-}" != "$STATE_DIR" ]; then
165
188
  if [ "$WAIT_LOCK" -gt 0 ]; then acquire_lock_wait || return $?; else acquire_lock || return 0; fi
166
189
  fi
@@ -180,7 +203,6 @@ refresh() {
180
203
  if [ -f "$SNAPSHOT" ]; then set_status 'stale: neither sqlite3 nor opencode is available; showing last good snapshot'; else set_status 'unavailable: neither sqlite3 nor opencode is available'; fi
181
204
  return 0
182
205
  fi
183
- mkdir -p "$EXTRACTIONS"
184
206
  local scratch sessions parts cache_records cache_state historical needs_extract out
185
207
  scratch=$(mktemp -d "$STATE_DIR/.refresh.XXXXXX")
186
208
  refresh_scratch=$scratch
@@ -199,6 +221,9 @@ refresh() {
199
221
  if [ -f "$SNAPSHOT" ]; then set_status 'stale: opencode database query failed; showing last good snapshot'; else set_status 'unavailable: opencode database query failed'; fi
200
222
  return 0
201
223
  }
224
+ # sqlite3 emits no bytes for zero rows. Publish an empty snapshot so removal
225
+ # of the final session also produces the non-actionable selection notice.
226
+ [ -s "$sessions" ] || printf '[]\n' > "$sessions"
202
227
  "$JQ_BIN" -e 'type == "array"' "$sessions" >/dev/null 2>&1 || {
203
228
  if [ -f "$SNAPSHOT" ]; then set_status 'stale: opencode database query failed; showing last good snapshot'; else set_status 'unavailable: opencode database query failed'; fi
204
229
  return 0
@@ -261,8 +286,9 @@ refresh() {
261
286
  updatedEpoch: (($updated / 1000 | floor) | if . > 0 then . else ($created / 1000 | floor) end),
262
287
  fulltextLower: $fulltext}') || record=''
263
288
  if [ -n "$record" ]; then
264
- printf '%s\n' "$record" > "$cache.tmp"
265
- mv "$cache.tmp" "$cache"
289
+ cache_tmp=$(mktemp "$EXTRACTIONS/$sid.json.XXXXXX")
290
+ printf '%s\n' "$record" > "$cache_tmp"
291
+ mv "$cache_tmp" "$cache"
266
292
  printf '%s\n' "$record" >> "$historical"
267
293
  fi
268
294
  done < "$needs_extract"
@@ -2,6 +2,7 @@
2
2
  # A short-lived worker owned by one picker. It serializes expensive refreshes;
3
3
  # fzf only ever asks the list script to render the last published snapshot.
4
4
  set -euo pipefail
5
+ umask 077
5
6
 
6
7
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7
8
  # The list script can be overridden for reuse; default is the opencode list.
@@ -14,6 +15,7 @@ SCOPE=''
14
15
  LIMIT=60
15
16
  TARGET=''
16
17
  TIMEOUT=12
18
+ SELECTED_ID=''
17
19
 
18
20
  usage() { echo "usage: ${0##*/} init|worker|render|request|wait --state-dir DIR [options]" >&2; exit 2; }
19
21
  while [ "$#" -gt 0 ]; do
@@ -22,6 +24,7 @@ while [ "$#" -gt 0 ]; do
22
24
  --scope) [ "$#" -ge 2 ] || usage; SCOPE=$2; shift ;;
23
25
  --limit) [ "$#" -ge 2 ] || usage; LIMIT=$2; shift ;;
24
26
  --query) [ "$#" -ge 2 ] || usage; QUERY=$2; shift ;;
27
+ --selected-id) [ "$#" -ge 2 ] || usage; SELECTED_ID=$2; shift ;;
25
28
  --target) [ "$#" -ge 2 ] || usage; TARGET=$2; shift ;;
26
29
  --timeout) [ "$#" -ge 2 ] || usage; TIMEOUT=$2; shift ;;
27
30
  *) usage ;;
@@ -49,7 +52,7 @@ case "$MODE" in
49
52
  atomic_text "$STATE_DIR/worker-complete" 0
50
53
  ;;
51
54
  render)
52
- "$LIST" --state-dir "$STATE_DIR" --query "${QUERY:-}"
55
+ "$LIST" --state-dir "$STATE_DIR" --query "${QUERY:-}" --selected-id "$SELECTED_ID"
53
56
  # An empty, first snapshot should not look like a broken picker.
54
57
  if [ ! -f "$STATE_DIR/status" ] && [ -f "$STATE_DIR/worker.pid" ]; then
55
58
  printf '\t\tunknown\tLoading sessions…\t\tnotice:loading\n'
@@ -5,7 +5,7 @@ sesh shortcuts
5
5
 
6
6
  Type Search session titles and transcript text
7
7
  Enter Resume the selected session here
8
- Ctrl-F Enter Resume the selected session as a fork
8
+ Ctrl-F Resume the selected session as a fork
9
9
  Ctrl-G Toggle current-directory scope / all sessions
10
10
  Space Toggle transcript preview
11
11
  ? Toggle this help
package/bin/sesh.sh CHANGED
@@ -4,6 +4,7 @@
4
4
  # Refreshes publish one state-dir snapshot; fzf query changes render that
5
5
  # snapshot only, so typing never starts competing scans.
6
6
  set -euo pipefail
7
+ umask 077
7
8
 
8
9
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9
10
 
@@ -25,8 +26,8 @@ version=$("$SESH_FZF" --version 2>/dev/null || true)
25
26
  if [[ "$version" =~ ^([0-9]+)\.([0-9]+)(\.([0-9]+))? ]]; then
26
27
  fzf_major=$((10#${BASH_REMATCH[1]}))
27
28
  fzf_minor=$((10#${BASH_REMATCH[2]}))
28
- if [ "$fzf_major" -eq 0 ] && [ "$fzf_minor" -lt 52 ]; then
29
- echo "sesh: fzf >= 0.52.0 is required (found $version)" >&2
29
+ if [ "$fzf_major" -eq 0 ] && [ "$fzf_minor" -lt 73 ]; then
30
+ echo "sesh: fzf >= 0.73.0 is required (found $version)" >&2
30
31
  exit 1
31
32
  fi
32
33
  else
@@ -92,7 +93,7 @@ cleanup() {
92
93
  trap cleanup EXIT
93
94
  trap 'exit 130' HUP INT TERM
94
95
 
95
- cache_cmd="$q_worker render --state-dir $q_state --query {q}"
96
+ cache_cmd="$q_worker render --state-dir $q_state --query {q} --selected-id {6}"
96
97
  preview_cmd="if [ -f $q_state/help ]; then $q_shortcuts; else $q_preview $q_state {2}; fi"
97
98
  help_bind="?:transform:[ -z {q} ] && ( [ -f $q_state/help ] && rm -f $q_state/help && echo hide-preview || ( : > $q_state/help && echo show-preview+refresh-preview ) ) || echo 'put(?)'"
98
99
  toggle_cmd="$q_list --toggle-scope --state-dir $q_state >/dev/null"
@@ -102,7 +103,7 @@ query="$picker_query"
102
103
  while :; do
103
104
  selection=''
104
105
  set +e
105
- selection=$(FZF_DEFAULT_COMMAND='printf ""' FZF_DEFAULT_OPTS='' FZF_DEFAULT_OPTS_FILE='' "$SESH_FZF" --print-query --expect=ctrl-f --query="$query" --ansi --delimiter=$'\t' --with-nth=4 --disabled --layout=reverse --info=hidden --prompt='❯ ' \
106
+ selection=$(FZF_DEFAULT_COMMAND='printf ""' FZF_DEFAULT_OPTS='' FZF_DEFAULT_OPTS_FILE='' "$SESH_FZF" --print-query --expect=ctrl-f --query="$query" --ansi --delimiter=$'\t' --with-nth=4 --track --id-nth=6 --disabled --layout=reverse --info=hidden --prompt='❯ ' \
106
107
  --preview="$preview_cmd" --preview-window='right,50%,wrap,hidden' \
107
108
  --bind='space:transform:[ -z {q} ] && echo toggle-preview || echo "put( )"' \
108
109
  --bind="$help_bind" \
@@ -113,10 +114,11 @@ selection=$(FZF_DEFAULT_COMMAND='printf ""' FZF_DEFAULT_OPTS='' FZF_DEFAULT_OPTS
113
114
  status=$?
114
115
  set -e
115
116
  case "$status" in 0) ;; 1|130) exit 0;; *) echo "sesh: picker failed ($status)" >&2; exit "$status";; esac
116
- key=${selection%%$'\n'*}
117
- selection=${selection#*$'\n'}
117
+ # --print-query precedes the --expect key (empty for Enter), then the row.
118
118
  query=${selection%%$'\n'*}
119
119
  selection=${selection#*$'\n'}
120
+ key=${selection%%$'\n'*}
121
+ selection=${selection#*$'\n'}
120
122
  session_id=$(printf '%s\n' "$selection" | cut -f2)
121
123
  [ -n "$session_id" ] || continue
122
124
  # Dispatch queues a new worker scan and waits for that exact serialized poll.
package/install.sh CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bash
2
2
  # Install sesh for the current user.
3
3
  # Terminal- and OS-agnostic: macOS and Linux, any terminal. No administrator
4
- # privileges, downloads, or shell-rc edits. Runtime requires jq, fzf >= 0.52
4
+ # privileges, downloads, or shell-rc edits. Runtime requires jq, fzf >= 0.73
5
5
  # and sqlite3 or the opencode CLI; the in-TUI panel additionally needs the
6
6
  # packages opencode installs for local plugins (handled below).
7
7
  set -euo pipefail
@@ -120,8 +120,8 @@ for tool in bash jq fzf; do
120
120
  done
121
121
  fzf_version=$(fzf --version 2>/dev/null || true)
122
122
  if [[ "$fzf_version" =~ ^([0-9]+)\.([0-9]+)(\.([0-9]+))? ]]; then
123
- if [ "$((10#${BASH_REMATCH[1]}))" -eq 0 ] && [ "$((10#${BASH_REMATCH[2]}))" -lt 52 ]; then
124
- fail "fzf >= 0.52.0 is required (found $fzf_version)."
123
+ if [ "$((10#${BASH_REMATCH[1]}))" -eq 0 ] && [ "$((10#${BASH_REMATCH[2]}))" -lt 73 ]; then
124
+ fail "fzf >= 0.73.0 is required (found $fzf_version)."
125
125
  fi
126
126
  else
127
127
  fail "cannot parse fzf version from: $fzf_version."
@@ -12,7 +12,10 @@ export default tool({
12
12
  args: {
13
13
  limit: tool.schema
14
14
  .number()
15
- .describe("Maximum sessions to return")
15
+ .int()
16
+ .min(0)
17
+ .max(1000)
18
+ .describe("Maximum sessions to return (0–1000; 0 returns none)")
16
19
  .default(50),
17
20
  directory: tool.schema
18
21
  .string()
@@ -20,23 +23,29 @@ export default tool({
20
23
  .optional(),
21
24
  },
22
25
  async execute(args) {
26
+ // Validate here as well as in the schema for direct/programmatic callers.
27
+ if (!Number.isInteger(args.limit) || args.limit < 0 || args.limit > 1000) {
28
+ throw new Error("limit must be an integer between 0 and 1000")
29
+ }
23
30
  const bun = (globalThis as any).Bun
24
31
  if (!bun || typeof bun.$ !== "function") {
25
32
  throw new Error("This tool requires the Bun runtime (globalThis.Bun.$)")
26
33
  }
27
34
 
28
- const out = await bun.$`opencode session list -n ${args.limit} --format json`.json()
29
- const sessions = (Array.isArray(out) ? out : []).filter(
30
- (s) => !args.directory || s.directory === args.directory,
31
- )
32
- return JSON.stringify(
33
- sessions.map((s) => ({
34
- id: s.id,
35
- title: s.title,
36
- directory: s.directory,
37
- updated: s.updated,
38
- created: s.created,
39
- })),
40
- )
35
+ // `session list` is project-scoped and hides only children of forks. The
36
+ // global store has no project instance, so query the SQLite session table
37
+ // directly (OpenCode 1.18.30). Match `Session.listGlobal`/the picker: roots
38
+ // only, non-archived. Bun quotes the SQL as one shell argument; SQL string
39
+ // literals need their own escaping as well.
40
+ const where = args.directory === undefined
41
+ ? ""
42
+ : ` AND directory = '${args.directory.replaceAll("'", "''")}'`
43
+ const query = `SELECT id, title, directory, time_updated AS updated, time_created AS created
44
+ FROM session WHERE parent_id IS NULL AND time_archived IS NULL${where}
45
+ ORDER BY time_updated DESC, id DESC LIMIT ${args.limit}`
46
+ const text = await bun.$`opencode db ${query} --format json`.text()
47
+ const sessions = JSON.parse(text.trim() || "[]")
48
+ if (!Array.isArray(sessions)) throw new Error("Unexpected opencode db result: expected an array")
49
+ return JSON.stringify(sessions)
41
50
  },
42
51
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-sesh",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "description": "Browse, search, preview and resume opencode sessions from the terminal and the TUI.",
6
6
  "license": "MIT",
@@ -40,17 +40,23 @@
40
40
  "node": ">=20"
41
41
  },
42
42
  "scripts": {
43
+ "prepare": "husky",
43
44
  "test": "bash tests/sesh.sh",
45
+ "test:picker": "python3 tests/picker.py",
44
46
  "typecheck": "tsc -p tsconfig.json",
45
- "lint:sh": "bash -n bin/*.sh bin/sesh install.sh tests/sesh.sh"
47
+ "lint:sh": "for script in bin/*.sh bin/sesh install.sh tests/*.sh; do bash -n \"$script\" || exit; done",
48
+ "prepublishOnly": "npm test && npm run typecheck"
46
49
  },
47
50
  "devDependencies": {
51
+ "@commitlint/cli": "^21.2.2",
52
+ "@commitlint/config-conventional": "^21.2.2",
48
53
  "@opencode-ai/plugin": "1.18.30",
49
54
  "@opencode-ai/sdk": "1.18.30",
50
55
  "@opentui/core": "^0.5.11",
51
56
  "@opentui/keymap": "^0.5.11",
52
57
  "@opentui/solid": "^0.5.11",
53
58
  "@types/node": "26.5.1",
59
+ "husky": "^9.1.7",
54
60
  "solid-js": "1.9.12",
55
61
  "typescript": "7.0.2"
56
62
  }