pi-sessions 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/README.md CHANGED
@@ -20,6 +20,8 @@
20
20
 
21
21
  ## Install
22
22
 
23
+ Requires Pi `0.78.1` or newer.
24
+
23
25
  **From npm** (recommended):
24
26
 
25
27
  ```bash
@@ -1,5 +1,6 @@
1
1
  import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
2
  import type { AutocompleteItem } from "@earendil-works/pi-tui";
3
+ import { isTuiMode } from "../shared/pi-mode.js";
3
4
 
4
5
  export const TITLE_USAGE = "Usage: /title [this|folder|pi] [-f]";
5
6
 
@@ -51,7 +52,7 @@ export function createSessionAutoTitleCommandHandler(
51
52
  ) => Promise<RetitleCommandOutcome>,
52
53
  ): (args: string, ctx: ExtensionCommandContext) => Promise<void> {
53
54
  return async (args: string, ctx: ExtensionCommandContext): Promise<void> => {
54
- const parsed = parseRetitleCommand(args, ctx.hasUI);
55
+ const parsed = parseRetitleCommand(args, isTuiMode(ctx));
55
56
  if (parsed.kind === "error") {
56
57
  ctx.ui.notify(parsed.message, "error");
57
58
  return;
@@ -76,10 +77,10 @@ export function getRetitleArgumentCompletions(argumentPrefix: string): Autocompl
76
77
  return filtered.length > 0 ? filtered : null;
77
78
  }
78
79
 
79
- export function parseRetitleCommand(args: string, hasUI: boolean): RetitleCommandParseResult {
80
+ export function parseRetitleCommand(args: string, hasTui: boolean): RetitleCommandParseResult {
80
81
  const trimmedArgs = args.trim();
81
82
  if (!trimmedArgs) {
82
- return hasUI ? { kind: "open-pane" } : { kind: "run", scope: "this" };
83
+ return hasTui ? { kind: "open-pane" } : { kind: "run", scope: "this" };
83
84
  }
84
85
 
85
86
  const tokens = trimmedArgs.split(/\s+/);
@@ -25,6 +25,7 @@ import {
25
25
  runRetitlePlan,
26
26
  } from "./session-auto-title/retitle.js";
27
27
  import { showRetitleWizard } from "./session-auto-title/wizard.js";
28
+ import { isTuiMode } from "./shared/pi-mode.js";
28
29
  import { loadSettings } from "./shared/settings.js";
29
30
 
30
31
  export {
@@ -160,7 +161,7 @@ async function handleTitleInvocation(
160
161
  };
161
162
 
162
163
  if (invocation.kind === "open-pane") {
163
- if (!ctx.hasUI) {
164
+ if (!isTuiMode(ctx)) {
164
165
  return retitleCurrentSession();
165
166
  }
166
167
 
@@ -171,7 +172,7 @@ async function handleTitleInvocation(
171
172
  return runWithInFlightTracking(state, retitleCurrentSession);
172
173
  }
173
174
 
174
- if (ctx.hasUI && !invocation.force) {
175
+ if (isTuiMode(ctx) && !invocation.force) {
175
176
  return showRetitleWizard(
176
177
  pi,
177
178
  state.controller,
@@ -1,7 +1,9 @@
1
+ import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
1
2
  import {
2
3
  type AssistantMessage,
3
- complete,
4
+ completeSimple,
4
5
  type Message,
6
+ type SimpleStreamOptions,
5
7
  type TextContent,
6
8
  type ThinkingContent,
7
9
  type Tool,
@@ -86,6 +88,7 @@ export interface HandoffDraftResult {
86
88
  export async function generateHandoffDraft(
87
89
  ctx: ExtensionContext,
88
90
  goal: string,
91
+ thinkingLevel: ThinkingLevel | undefined,
89
92
  signal?: AbortSignal,
90
93
  ): Promise<HandoffDraftResult | undefined> {
91
94
  if (!ctx.model) {
@@ -117,25 +120,23 @@ export async function generateHandoffDraft(
117
120
  timestamp: Date.now(),
118
121
  };
119
122
 
120
- const response = await complete(
123
+ const requestOptions: SimpleStreamOptions = {
124
+ apiKey: auth.apiKey,
125
+ ...(auth.headers ? { headers: auth.headers } : {}),
126
+ ...(signal ? { signal } : {}),
127
+ ...(ctx.model.reasoning && thinkingLevel && thinkingLevel !== "off"
128
+ ? { reasoning: thinkingLevel }
129
+ : {}),
130
+ };
131
+
132
+ const response = await completeSimple(
121
133
  ctx.model,
122
134
  {
123
135
  systemPrompt: HANDOFF_SYSTEM_PROMPT,
124
136
  messages: [userMessage],
125
137
  tools: [HANDOFF_EXTRACTION_TOOL],
126
138
  },
127
- signal
128
- ? {
129
- apiKey: auth.apiKey,
130
- ...(auth.headers ? { headers: auth.headers } : {}),
131
- signal,
132
- toolChoice: "any",
133
- }
134
- : {
135
- apiKey: auth.apiKey,
136
- ...(auth.headers ? { headers: auth.headers } : {}),
137
- toolChoice: "any",
138
- },
139
+ requestOptions,
139
140
  );
140
141
 
141
142
  if (response.stopReason === "aborted") {
@@ -23,6 +23,7 @@ import {
23
23
  launchSplitHandoffSession,
24
24
  validateSplitHandoffPrerequisites,
25
25
  } from "./session-handoff/spawn.js";
26
+ import { isTuiMode } from "./shared/pi-mode.js";
26
27
  import { loadSettings } from "./shared/settings.js";
27
28
 
28
29
  const HANDOFF_USAGE = "Usage: /handoff [--left|--right|--up|--down] <goal for new thread>";
@@ -38,7 +39,7 @@ export default function sessionHandoffExtension(pi: ExtensionAPI): void {
38
39
  pi.registerCommand("handoff", {
39
40
  description: "Transfer context to a new focused session",
40
41
  handler: async (args: string, ctx: ExtensionCommandContext): Promise<void> => {
41
- if (!ctx.hasUI) {
42
+ if (!isTuiMode(ctx)) {
42
43
  ctx.ui.notify("handoff requires interactive mode", "error");
43
44
  return;
44
45
  }
@@ -76,7 +77,8 @@ export default function sessionHandoffExtension(pi: ExtensionAPI): void {
76
77
  generatedDraft = await runWithLoader(
77
78
  ctx,
78
79
  "Generating handoff draft...",
79
- async (signal: AbortSignal) => generateHandoffDraft(ctx, parsedArgs.goal, signal),
80
+ async (signal: AbortSignal) =>
81
+ generateHandoffDraft(ctx, parsedArgs.goal, pi.getThinkingLevel(), signal),
80
82
  );
81
83
  } catch (error) {
82
84
  ctx.ui.notify(formatHandoffError(error), "error");
@@ -157,7 +159,7 @@ export default function sessionHandoffExtension(pi: ExtensionAPI): void {
157
159
  pi.registerShortcut(settings.handoff.pickerShortcut, {
158
160
  description: "Open the session reference picker",
159
161
  handler: async (ctx) => {
160
- if (!ctx.hasUI) {
162
+ if (!isTuiMode(ctx)) {
161
163
  return;
162
164
  }
163
165
 
@@ -6,6 +6,7 @@ import {
6
6
  } from "@earendil-works/pi-coding-agent";
7
7
  import { type Focusable, matchesKey, visibleWidth } from "@earendil-works/pi-tui";
8
8
  import { type ReindexResult, rebuildSessionIndex } from "./session-search/reindex.js";
9
+ import { isTuiMode } from "./shared/pi-mode.js";
9
10
  import { getIndexStatus, type SessionIndexStatus } from "./shared/session-index/index.js";
10
11
  import { loadSettings } from "./shared/settings.js";
11
12
 
@@ -17,7 +18,7 @@ export default function sessionIndexExtension(pi: ExtensionAPI): void {
17
18
  pi.registerCommand("session-index", {
18
19
  description: "Open the session index control panel",
19
20
  handler: async (_args, ctx) => {
20
- if (!ctx.hasUI) {
21
+ if (!isTuiMode(ctx)) {
21
22
  ctx.ui.notify("/session-index requires interactive mode.", "warning");
22
23
  return;
23
24
  }
@@ -1,4 +1,4 @@
1
- import { renameSync } from "node:fs";
1
+ import { renameSync, rmSync } from "node:fs";
2
2
  import { SessionManager } from "@earendil-works/pi-coding-agent";
3
3
  import {
4
4
  createTempIndexPath,
@@ -34,11 +34,16 @@ export async function rebuildSessionIndex(options: ReindexOptions): Promise<Rein
34
34
  try {
35
35
  initializeSchema(db);
36
36
  ({ sessionCount, chunkCount } = indexSessionFiles(db, sessionFiles));
37
+ // Fold the WAL into the main file so the single-file rename below is
38
+ // self-contained. bun:sqlite does not checkpoint on close.
39
+ db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
37
40
  } finally {
38
41
  db.close();
39
42
  }
40
43
 
41
44
  renameSync(tempIndexPath, finalIndexPath);
45
+ rmSync(`${tempIndexPath}-wal`, { force: true });
46
+ rmSync(`${tempIndexPath}-shm`, { force: true });
42
47
  return { sessionCount, chunkCount, indexPath: finalIndexPath };
43
48
  }
44
49
 
@@ -0,0 +1,5 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+
3
+ export function isTuiMode(ctx: Pick<ExtensionContext, "mode">): boolean {
4
+ return ctx.mode === "tui";
5
+ }
@@ -1,7 +1,7 @@
1
- import type Database from "better-sqlite3";
2
1
  import { Type } from "typebox";
3
2
  import type { FileTouchOp, FileTouchSource, PathScope } from "../../session-search/normalize.js";
4
3
  import { safeParseTypeBoxJson } from "../typebox.js";
4
+ import type { SqliteDatabase } from "./sqlite.js";
5
5
 
6
6
  export const INDEX_SCHEMA_VERSION = 7;
7
7
 
@@ -119,7 +119,7 @@ export interface SessionIndexStatus {
119
119
  lastFullReindexAt?: string | undefined;
120
120
  }
121
121
 
122
- export type SessionIndexDatabase = Database.Database;
122
+ export type SessionIndexDatabase = SqliteDatabase;
123
123
 
124
124
  export const NULLABLE_STRING_SCHEMA = Type.Union([Type.String(), Type.Null()]);
125
125
  export const SESSION_ORIGIN_SCHEMA = Type.Union([
@@ -113,7 +113,7 @@ export function getSessionById(
113
113
  )
114
114
  .get(sessionId);
115
115
 
116
- if (row === undefined) {
116
+ if (row === undefined || row === null) {
117
117
  return undefined;
118
118
  }
119
119
 
@@ -140,7 +140,7 @@ export function getSessionByPath(
140
140
  )
141
141
  .get(sessionPath);
142
142
 
143
- if (row === undefined) {
143
+ if (row === undefined || row === null) {
144
144
  return undefined;
145
145
  }
146
146
 
@@ -1,6 +1,5 @@
1
1
  import { existsSync, mkdirSync } from "node:fs";
2
2
  import path from "node:path";
3
- import Database from "better-sqlite3";
4
3
  import { parseTypeBoxValue } from "../typebox.js";
5
4
  import {
6
5
  INDEX_SCHEMA_VERSION,
@@ -9,6 +8,7 @@ import {
9
8
  type SessionIndexDatabase,
10
9
  type SessionIndexStatus,
11
10
  } from "./common.js";
11
+ import { openSqlite } from "./sqlite.js";
12
12
 
13
13
  export function ensureIndexDir(dir: string): string {
14
14
  if (!existsSync(dir)) {
@@ -29,16 +29,7 @@ export function openIndexDatabase(
29
29
  options?: { create?: boolean; timeoutMs?: number },
30
30
  ): SessionIndexDatabase {
31
31
  const create = options?.create ?? true;
32
- const db = new Database(
33
- dbPath,
34
- options?.timeoutMs === undefined
35
- ? { fileMustExist: !create }
36
- : { fileMustExist: !create, timeout: options.timeoutMs },
37
- );
38
- db.pragma("journal_mode = WAL");
39
- db.pragma("synchronous = NORMAL");
40
- db.pragma("foreign_keys = ON");
41
- return db;
32
+ return openSqlite(dbPath, { create, timeoutMs: options?.timeoutMs });
42
33
  }
43
34
 
44
35
  export function initializeSchema(db: SessionIndexDatabase): void {
@@ -148,7 +139,7 @@ export function setMetadata(db: SessionIndexDatabase, key: string, value: string
148
139
 
149
140
  export function getMetadata(db: SessionIndexDatabase, key: string): string | undefined {
150
141
  const row = db.prepare(`SELECT value FROM metadata WHERE key = ?`).get(key);
151
- if (row === undefined) {
142
+ if (row === undefined || row === null) {
152
143
  return undefined;
153
144
  }
154
145
 
@@ -0,0 +1,65 @@
1
+ import { createRequire } from "node:module";
2
+
3
+ const requireModule = createRequire(import.meta.url);
4
+ const isBun = Boolean(process.versions.bun);
5
+
6
+ export type SqliteBindValue = string | number | bigint | null;
7
+
8
+ export interface SqliteRunResult {
9
+ changes: number;
10
+ lastInsertRowid: number | bigint;
11
+ }
12
+
13
+ export interface SqliteStatement {
14
+ get(...params: SqliteBindValue[]): unknown;
15
+ all(...params: SqliteBindValue[]): unknown[];
16
+ run(...params: SqliteBindValue[]): SqliteRunResult;
17
+ }
18
+
19
+ export interface SqliteDatabase {
20
+ readonly inTransaction: boolean;
21
+ prepare(sql: string): SqliteStatement;
22
+ exec(sql: string): void;
23
+ transaction<Args extends unknown[], Result>(
24
+ fn: (...args: Args) => Result,
25
+ ): (...args: Args) => Result;
26
+ close(): void;
27
+ }
28
+
29
+ interface SqliteConstructor {
30
+ new (path: string, options?: Record<string, unknown>): SqliteDatabase;
31
+ }
32
+
33
+ // The driver is resolved at runtime so Bun never loads the native better-sqlite3
34
+ // addon (unsupported) and Node never loads the bun:sqlite builtin. The require
35
+ // result is untyped, so the cast to our structural contract is unavoidable here.
36
+ function loadDatabaseConstructor(): SqliteConstructor {
37
+ if (isBun) {
38
+ return (requireModule("bun:sqlite") as { Database: SqliteConstructor }).Database;
39
+ }
40
+ return requireModule("better-sqlite3") as SqliteConstructor;
41
+ }
42
+
43
+ export function openSqlite(
44
+ dbPath: string,
45
+ options: { create: boolean; timeoutMs?: number | undefined },
46
+ ): SqliteDatabase {
47
+ const Database = loadDatabaseConstructor();
48
+ const db = isBun
49
+ ? new Database(dbPath, { create: options.create, readwrite: true })
50
+ : new Database(
51
+ dbPath,
52
+ options.timeoutMs === undefined
53
+ ? { fileMustExist: !options.create }
54
+ : { fileMustExist: !options.create, timeout: options.timeoutMs },
55
+ );
56
+
57
+ db.exec("PRAGMA journal_mode = WAL");
58
+ db.exec("PRAGMA synchronous = NORMAL");
59
+ db.exec("PRAGMA foreign_keys = ON");
60
+ if (isBun && options.timeoutMs !== undefined) {
61
+ db.exec(`PRAGMA busy_timeout = ${options.timeoutMs}`);
62
+ }
63
+
64
+ return db;
65
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-sessions",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "type": "module",
5
5
  "description": "Pi session search, ask, handoff, auto-titling, and indexing tools",
6
6
  "license": "MIT",
@@ -38,15 +38,14 @@
38
38
  "typecheck": "tsc --noEmit",
39
39
  "test": "vitest run",
40
40
  "check": "biome check . && tsc --noEmit && vitest run",
41
- "prepare": "husky"
41
+ "prepare": "husky || true"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@biomejs/biome": "^2.4.14",
45
- "@earendil-works/pi-agent-core": "^0.78.0",
46
- "@earendil-works/pi-ai": "^0.78.0",
47
- "@earendil-works/pi-coding-agent": "^0.78.0",
48
- "@earendil-works/pi-tui": "^0.78.0",
49
- "@types/better-sqlite3": "^7.6.13",
45
+ "@earendil-works/pi-agent-core": "^0.78.1",
46
+ "@earendil-works/pi-ai": "^0.78.1",
47
+ "@earendil-works/pi-coding-agent": "^0.78.1",
48
+ "@earendil-works/pi-tui": "^0.78.1",
50
49
  "@types/node": "^25.6.2",
51
50
  "husky": "^9.1.7",
52
51
  "lint-staged": "^17.0.3",
@@ -55,16 +54,16 @@
55
54
  "vitest": "^4.1.5"
56
55
  },
57
56
  "peerDependencies": {
58
- "@earendil-works/pi-agent-core": ">=0.78.0",
59
- "@earendil-works/pi-ai": ">=0.78.0",
60
- "@earendil-works/pi-coding-agent": ">=0.78.0",
61
- "@earendil-works/pi-tui": ">=0.78.0",
57
+ "@earendil-works/pi-agent-core": ">=0.78.1",
58
+ "@earendil-works/pi-ai": ">=0.78.1",
59
+ "@earendil-works/pi-coding-agent": ">=0.78.1",
60
+ "@earendil-works/pi-tui": ">=0.78.1",
62
61
  "typebox": ">=1.1.24"
63
62
  },
64
63
  "lint-staged": {
65
64
  "*.{js,cjs,mjs,jsx,ts,tsx,json,jsonc}": "biome check --write --no-errors-on-unmatched"
66
65
  },
67
- "dependencies": {
66
+ "optionalDependencies": {
68
67
  "better-sqlite3": "^12.9.0"
69
68
  }
70
69
  }