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 +2 -0
- package/extensions/session-auto-title/command.ts +4 -3
- package/extensions/session-auto-title.ts +3 -2
- package/extensions/session-handoff/extract.ts +15 -14
- package/extensions/session-handoff.ts +5 -3
- package/extensions/session-index.ts +2 -1
- package/extensions/session-search/reindex.ts +6 -1
- package/extensions/shared/pi-mode.ts +5 -0
- package/extensions/shared/session-index/common.ts +2 -2
- package/extensions/shared/session-index/lineage.ts +2 -2
- package/extensions/shared/session-index/schema.ts +3 -12
- package/extensions/shared/session-index/sqlite.ts +65 -0
- package/package.json +11 -12
package/README.md
CHANGED
|
@@ -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
|
|
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,
|
|
80
|
+
export function parseRetitleCommand(args: string, hasTui: boolean): RetitleCommandParseResult {
|
|
80
81
|
const trimmedArgs = args.trim();
|
|
81
82
|
if (!trimmedArgs) {
|
|
82
|
-
return
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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) =>
|
|
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
|
|
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
|
|
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
|
|
|
@@ -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 =
|
|
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
|
-
|
|
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.
|
|
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.
|
|
46
|
-
"@earendil-works/pi-ai": "^0.78.
|
|
47
|
-
"@earendil-works/pi-coding-agent": "^0.78.
|
|
48
|
-
"@earendil-works/pi-tui": "^0.78.
|
|
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.
|
|
59
|
-
"@earendil-works/pi-ai": ">=0.78.
|
|
60
|
-
"@earendil-works/pi-coding-agent": ">=0.78.
|
|
61
|
-
"@earendil-works/pi-tui": ">=0.78.
|
|
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
|
-
"
|
|
66
|
+
"optionalDependencies": {
|
|
68
67
|
"better-sqlite3": "^12.9.0"
|
|
69
68
|
}
|
|
70
69
|
}
|