pi-sessions 0.1.0 → 0.2.1
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 +12 -5
- package/extensions/session-handoff/metadata.ts +46 -42
- package/extensions/session-handoff/spawn.ts +142 -0
- package/extensions/session-handoff.ts +158 -33
- package/extensions/session-hooks.ts +22 -2
- package/extensions/session-search/extract.ts +2 -2
- package/extensions/session-search/hooks.ts +105 -40
- package/extensions/shared/session-index/schema.ts +7 -2
- package/extensions/shared/settings.ts +2 -2
- package/package.json +14 -13
package/README.md
CHANGED
|
@@ -62,11 +62,20 @@ What session did I implement the db layer?
|
|
|
62
62
|
|
|
63
63
|
`/handoff <goal>` starts a focused new session. Give pi a goal, and it will generate a prompt for you to review before kicking it off.
|
|
64
64
|
|
|
65
|
+
You can either start a new session directly in your current one, or if you have Ghostty on macOS, you can spawn a new one in a split-pane and continue in your current:
|
|
66
|
+
|
|
67
|
+
- `/handoff --left <goal>`
|
|
68
|
+
- `/handoff --right <goal>`
|
|
69
|
+
- `/handoff --up <goal>`
|
|
70
|
+
- `/handoff --down <goal>`
|
|
71
|
+
|
|
72
|
+
The flag indicates the Ghostty split direction.
|
|
73
|
+
|
|
65
74
|
Flow:
|
|
66
75
|
|
|
67
|
-
- run `/handoff <goal>`
|
|
76
|
+
- run `/handoff [--<direction>] <goal>`
|
|
68
77
|
- review the generated prompt preview
|
|
69
|
-
- optionally edit the prompt
|
|
78
|
+
- optionally edit the prompt
|
|
70
79
|
- start the new session
|
|
71
80
|
|
|
72
81
|
If you do nothing, the preview autostarts after a short countdown.
|
|
@@ -138,8 +147,6 @@ To manage existing titles, run `/title`, where you can:
|
|
|
138
147
|
|
|
139
148
|
Note that generating titles for all sessions can take some time, and will hit your configured model with the full contents of all sessions.
|
|
140
149
|
|
|
141
|
-
Behavior notes:
|
|
142
|
-
|
|
143
150
|
- automatic retitles run every few turns
|
|
144
151
|
- if you manually rename a session with `/name`, automatic retitling pauses for that session
|
|
145
152
|
- Regenerate the title for the current session to resume automatic retitling
|
|
@@ -147,7 +154,7 @@ Behavior notes:
|
|
|
147
154
|
- `google/gemini-flash-lite-latest`
|
|
148
155
|
- `anthropic/claude-haiku-4-5`
|
|
149
156
|
- `openai/gpt-5.4-mini`
|
|
150
|
-
- your currently configured model
|
|
157
|
+
- your currently configured model
|
|
151
158
|
|
|
152
159
|
To change auto-titling settings, edit `~/.pi/agent/settings.json`:
|
|
153
160
|
|
|
@@ -1,25 +1,29 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
2
|
import type { CustomEntry, SessionEntry } from "@mariozechner/pi-coding-agent";
|
|
3
3
|
import { type Static, Type } from "@sinclair/typebox";
|
|
4
4
|
import { safeParseTypeBoxValue } from "../shared/typebox.js";
|
|
5
5
|
|
|
6
6
|
export const HANDOFF_METADATA_CUSTOM_TYPE = "pi-sessions.handoff";
|
|
7
|
-
export const
|
|
7
|
+
export const HANDOFF_BOOTSTRAP_ENV = "PI_SESSIONS_HANDOFF_BOOTSTRAP";
|
|
8
|
+
export const HANDOFF_STALE_SESSION_MESSAGE =
|
|
9
|
+
"Session handoff failed: target session already has user input.";
|
|
8
10
|
|
|
9
11
|
export const HANDOFF_SESSION_METADATA_SCHEMA = Type.Object({
|
|
10
12
|
origin: Type.Literal("handoff"),
|
|
11
13
|
goal: Type.String(),
|
|
12
14
|
nextTask: Type.String(),
|
|
13
15
|
initial_prompt: Type.String(),
|
|
14
|
-
initial_prompt_nonce: Type.String(),
|
|
15
16
|
});
|
|
16
17
|
|
|
17
|
-
export const
|
|
18
|
-
|
|
18
|
+
export const HANDOFF_BOOTSTRAP_SCHEMA = Type.Object({
|
|
19
|
+
sessionId: Type.String(),
|
|
20
|
+
goal: Type.String(),
|
|
21
|
+
nextTask: Type.String(),
|
|
22
|
+
initialPrompt: Type.String(),
|
|
19
23
|
});
|
|
20
24
|
|
|
21
25
|
export type HandoffSessionMetadata = Static<typeof HANDOFF_SESSION_METADATA_SCHEMA>;
|
|
22
|
-
export type
|
|
26
|
+
export type HandoffBootstrap = Static<typeof HANDOFF_BOOTSTRAP_SCHEMA>;
|
|
23
27
|
|
|
24
28
|
export function createHandoffSessionMetadata(
|
|
25
29
|
goal: string,
|
|
@@ -28,69 +32,69 @@ export function createHandoffSessionMetadata(
|
|
|
28
32
|
): HandoffSessionMetadata {
|
|
29
33
|
const normalizedGoal = goal.trim();
|
|
30
34
|
const normalizedNextTask = nextTask.trim() || normalizedGoal;
|
|
31
|
-
const normalizedInitialPrompt = initialPrompt.trim();
|
|
32
35
|
|
|
33
36
|
return {
|
|
34
37
|
origin: "handoff",
|
|
35
38
|
goal: normalizedGoal,
|
|
36
39
|
nextTask: normalizedNextTask,
|
|
37
|
-
initial_prompt:
|
|
38
|
-
|
|
40
|
+
initial_prompt: initialPrompt.trim(),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createHandoffBootstrap(
|
|
45
|
+
sessionId: string,
|
|
46
|
+
metadata: HandoffSessionMetadata,
|
|
47
|
+
): HandoffBootstrap {
|
|
48
|
+
return {
|
|
49
|
+
sessionId,
|
|
50
|
+
goal: metadata.goal,
|
|
51
|
+
nextTask: metadata.nextTask,
|
|
52
|
+
initialPrompt: metadata.initial_prompt,
|
|
39
53
|
};
|
|
40
54
|
}
|
|
41
55
|
|
|
42
|
-
export function
|
|
43
|
-
return
|
|
56
|
+
export function encodeHandoffBootstrap(bootstrap: HandoffBootstrap): string {
|
|
57
|
+
return Buffer.from(JSON.stringify(bootstrap), "utf8").toString("base64");
|
|
44
58
|
}
|
|
45
59
|
|
|
46
|
-
export function
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
60
|
+
export function parseHandoffBootstrap(value: string): HandoffBootstrap | undefined {
|
|
61
|
+
try {
|
|
62
|
+
const decoded = Buffer.from(value, "base64").toString("utf8");
|
|
63
|
+
return safeParseTypeBoxValue(HANDOFF_BOOTSTRAP_SCHEMA, JSON.parse(decoded));
|
|
64
|
+
} catch {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
50
67
|
}
|
|
51
68
|
|
|
52
69
|
export function parseHandoffSessionMetadata(value: unknown): HandoffSessionMetadata | undefined {
|
|
53
70
|
return safeParseTypeBoxValue(HANDOFF_SESSION_METADATA_SCHEMA, value);
|
|
54
71
|
}
|
|
55
72
|
|
|
56
|
-
export function
|
|
73
|
+
export function getHandoffMetadataFromEntries(
|
|
57
74
|
entries: readonly SessionEntry[],
|
|
58
75
|
): HandoffSessionMetadata | undefined {
|
|
59
|
-
const consumed = new Set<string>();
|
|
60
|
-
let pending: HandoffSessionMetadata | undefined;
|
|
61
|
-
|
|
62
76
|
for (const entry of entries) {
|
|
63
77
|
if (entry.type !== "custom") {
|
|
64
78
|
continue;
|
|
65
79
|
}
|
|
66
80
|
|
|
67
|
-
const
|
|
68
|
-
if (
|
|
69
|
-
|
|
70
|
-
if (consumedEntry) {
|
|
71
|
-
consumed.add(consumedEntry.nonce);
|
|
72
|
-
if (pending?.initial_prompt_nonce === consumedEntry.nonce) {
|
|
73
|
-
pending = undefined;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
continue;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (customEntry.customType !== HANDOFF_METADATA_CUSTOM_TYPE) {
|
|
80
|
-
continue;
|
|
81
|
+
const metadata = parseCustomHandoffMetadata(entry);
|
|
82
|
+
if (metadata) {
|
|
83
|
+
return metadata;
|
|
81
84
|
}
|
|
85
|
+
}
|
|
82
86
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
87
89
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
90
|
+
export function hasUserMessages(entries: readonly SessionEntry[]): boolean {
|
|
91
|
+
return entries.some((entry) => entry.type === "message" && entry.message.role === "user");
|
|
92
|
+
}
|
|
91
93
|
|
|
92
|
-
|
|
94
|
+
function parseCustomHandoffMetadata(entry: CustomEntry): HandoffSessionMetadata | undefined {
|
|
95
|
+
if (entry.customType !== HANDOFF_METADATA_CUSTOM_TYPE) {
|
|
96
|
+
return undefined;
|
|
93
97
|
}
|
|
94
98
|
|
|
95
|
-
return
|
|
99
|
+
return parseHandoffSessionMetadata(entry.data);
|
|
96
100
|
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
CURRENT_SESSION_VERSION,
|
|
6
|
+
type ExtensionAPI,
|
|
7
|
+
type ExtensionCommandContext,
|
|
8
|
+
type SessionHeader,
|
|
9
|
+
} from "@mariozechner/pi-coding-agent";
|
|
10
|
+
import { HANDOFF_BOOTSTRAP_ENV } from "./metadata.js";
|
|
11
|
+
|
|
12
|
+
const GHOSTTY_MACOS_ONLY_MESSAGE = "Split handoff currently supports Ghostty on macOS only.";
|
|
13
|
+
const GHOSTTY_REQUIRED_MESSAGE = "Split handoff requires running inside Ghostty.";
|
|
14
|
+
const GHOSTTY_SPLIT_TIMEOUT_MS = 15_000;
|
|
15
|
+
const OSASCRIPT_PATH = "/usr/bin/osascript";
|
|
16
|
+
|
|
17
|
+
export type HandoffSplitDirection = "left" | "right" | "up" | "down";
|
|
18
|
+
|
|
19
|
+
export interface CreatedHandoffSession {
|
|
20
|
+
sessionId: string;
|
|
21
|
+
sessionFile: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function validateSplitHandoffPrerequisites(
|
|
25
|
+
_pi: ExtensionAPI,
|
|
26
|
+
ctx: ExtensionCommandContext,
|
|
27
|
+
): Promise<string | undefined> {
|
|
28
|
+
if (!ctx.sessionManager.getSessionFile()) {
|
|
29
|
+
return "Split handoff requires a persisted current session.";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (process.platform !== "darwin") {
|
|
33
|
+
return GHOSTTY_MACOS_ONLY_MESSAGE;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (process.env.TERM_PROGRAM !== "ghostty") {
|
|
37
|
+
return GHOSTTY_REQUIRED_MESSAGE;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function createHandoffSession(options: {
|
|
44
|
+
cwd: string;
|
|
45
|
+
sessionDir: string;
|
|
46
|
+
parentSessionFile: string;
|
|
47
|
+
}): CreatedHandoffSession {
|
|
48
|
+
const sessionId = randomUUID();
|
|
49
|
+
const timestamp = new Date().toISOString();
|
|
50
|
+
const fileTimestamp = timestamp.replace(/[:.]/g, "-");
|
|
51
|
+
const sessionFile = join(options.sessionDir, `${fileTimestamp}_${sessionId}.jsonl`);
|
|
52
|
+
|
|
53
|
+
const header: SessionHeader = {
|
|
54
|
+
type: "session",
|
|
55
|
+
version: CURRENT_SESSION_VERSION,
|
|
56
|
+
id: sessionId,
|
|
57
|
+
timestamp,
|
|
58
|
+
cwd: options.cwd,
|
|
59
|
+
parentSession: options.parentSessionFile,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
writeFileSync(sessionFile, `${JSON.stringify(header)}\n`);
|
|
63
|
+
|
|
64
|
+
return { sessionId, sessionFile };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function launchSplitHandoffSession(
|
|
68
|
+
pi: ExtensionAPI,
|
|
69
|
+
options: {
|
|
70
|
+
cwd: string;
|
|
71
|
+
sessionDir: string;
|
|
72
|
+
direction: HandoffSplitDirection;
|
|
73
|
+
sessionId: string;
|
|
74
|
+
bootstrapValue: string;
|
|
75
|
+
},
|
|
76
|
+
): Promise<{ success: true } | { success: false; error: string }> {
|
|
77
|
+
const piCommand = buildPiLaunchCommand(
|
|
78
|
+
options.sessionDir,
|
|
79
|
+
options.sessionId,
|
|
80
|
+
options.bootstrapValue,
|
|
81
|
+
);
|
|
82
|
+
const escapedCwd = escapeAppleScriptString(options.cwd);
|
|
83
|
+
const escapedCommand = escapeAppleScriptString(piCommand);
|
|
84
|
+
const appleScript = [
|
|
85
|
+
'tell application "Ghostty"',
|
|
86
|
+
" set targetTerminal to focused terminal of selected tab of front window",
|
|
87
|
+
" set cfg to new surface configuration",
|
|
88
|
+
` set initial working directory of cfg to "${escapedCwd}"`,
|
|
89
|
+
` set command of cfg to "${escapedCommand}"`,
|
|
90
|
+
` set newTerminal to split targetTerminal direction ${options.direction} with configuration cfg`,
|
|
91
|
+
" focus targetTerminal",
|
|
92
|
+
"end tell",
|
|
93
|
+
].join("\n");
|
|
94
|
+
const result = await pi.exec(OSASCRIPT_PATH, ["-e", appleScript], {
|
|
95
|
+
cwd: options.cwd,
|
|
96
|
+
timeout: GHOSTTY_SPLIT_TIMEOUT_MS,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (result.code === 0) {
|
|
100
|
+
return { success: true };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const details = `${result.stderr || result.stdout}`.trim() || `exit code ${result.code}`;
|
|
104
|
+
return {
|
|
105
|
+
success: false,
|
|
106
|
+
error:
|
|
107
|
+
`Failed to launch Ghostty split: ${details}. ` +
|
|
108
|
+
"Split handoff currently supports Ghostty on macOS only.",
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function buildPiResumeCommand(
|
|
113
|
+
sessionDir: string,
|
|
114
|
+
sessionId: string,
|
|
115
|
+
bootstrapValue: string,
|
|
116
|
+
): string {
|
|
117
|
+
return [
|
|
118
|
+
`${HANDOFF_BOOTSTRAP_ENV}=${shellQuote(bootstrapValue)}`,
|
|
119
|
+
"pi",
|
|
120
|
+
"--session-dir",
|
|
121
|
+
shellQuote(sessionDir),
|
|
122
|
+
"--session",
|
|
123
|
+
shellQuote(sessionId),
|
|
124
|
+
].join(" ");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function buildPiLaunchCommand(
|
|
128
|
+
sessionDir: string,
|
|
129
|
+
sessionId: string,
|
|
130
|
+
bootstrapValue: string,
|
|
131
|
+
): string {
|
|
132
|
+
const payload = `${buildPiResumeCommand(sessionDir, sessionId, bootstrapValue)}; exec /bin/zsh -il`;
|
|
133
|
+
return `/bin/zsh -ilc ${shellQuote(payload)}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function escapeAppleScriptString(value: string): string {
|
|
137
|
+
return value.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function shellQuote(value: string): string {
|
|
141
|
+
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
142
|
+
}
|
|
@@ -1,23 +1,32 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ExtensionAPI,
|
|
3
|
-
ExtensionCommandContext,
|
|
4
|
-
SessionManager,
|
|
5
|
-
} from "@mariozechner/pi-coding-agent";
|
|
1
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
6
2
|
import { buildSessionContext } from "@mariozechner/pi-coding-agent";
|
|
7
3
|
import { Key, matchesKey } from "@mariozechner/pi-tui";
|
|
8
4
|
import { generateHandoffDraft, type HandoffDraftResult } from "./session-handoff/extract.js";
|
|
9
5
|
import {
|
|
6
|
+
createHandoffBootstrap,
|
|
10
7
|
createHandoffSessionMetadata,
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
encodeHandoffBootstrap,
|
|
9
|
+
getHandoffMetadataFromEntries,
|
|
10
|
+
HANDOFF_BOOTSTRAP_ENV,
|
|
13
11
|
HANDOFF_METADATA_CUSTOM_TYPE,
|
|
14
|
-
|
|
12
|
+
HANDOFF_STALE_SESSION_MESSAGE,
|
|
13
|
+
hasUserMessages,
|
|
14
|
+
parseHandoffBootstrap,
|
|
15
15
|
} from "./session-handoff/metadata.js";
|
|
16
16
|
import { openSessionReferencePicker } from "./session-handoff/picker.js";
|
|
17
17
|
import { SESSION_TOKEN_PREFIX } from "./session-handoff/query.js";
|
|
18
18
|
import { renderStrongModal, reviewHandoffDraft } from "./session-handoff/review.js";
|
|
19
|
+
import {
|
|
20
|
+
buildPiResumeCommand,
|
|
21
|
+
createHandoffSession,
|
|
22
|
+
type HandoffSplitDirection,
|
|
23
|
+
launchSplitHandoffSession,
|
|
24
|
+
validateSplitHandoffPrerequisites,
|
|
25
|
+
} from "./session-handoff/spawn.js";
|
|
19
26
|
import { loadSettings } from "./shared/settings.js";
|
|
20
27
|
|
|
28
|
+
const HANDOFF_USAGE = "Usage: /handoff [--left|--right|--up|--down] <goal for new thread>";
|
|
29
|
+
|
|
21
30
|
export default function sessionHandoffExtension(pi: ExtensionAPI): void {
|
|
22
31
|
const settings = loadSettings();
|
|
23
32
|
|
|
@@ -34,9 +43,9 @@ export default function sessionHandoffExtension(pi: ExtensionAPI): void {
|
|
|
34
43
|
return;
|
|
35
44
|
}
|
|
36
45
|
|
|
37
|
-
const
|
|
38
|
-
if (
|
|
39
|
-
ctx.ui.notify(
|
|
46
|
+
const parsedArgs = parseHandoffCommandArgs(args);
|
|
47
|
+
if (parsedArgs.kind === "error") {
|
|
48
|
+
ctx.ui.notify(parsedArgs.message, "error");
|
|
40
49
|
return;
|
|
41
50
|
}
|
|
42
51
|
|
|
@@ -49,12 +58,20 @@ export default function sessionHandoffExtension(pi: ExtensionAPI): void {
|
|
|
49
58
|
return;
|
|
50
59
|
}
|
|
51
60
|
|
|
61
|
+
if (parsedArgs.splitDirection) {
|
|
62
|
+
const preflightError = await validateSplitHandoffPrerequisites(pi, ctx);
|
|
63
|
+
if (preflightError) {
|
|
64
|
+
ctx.ui.notify(preflightError, "error");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
52
69
|
let generatedDraft: HandoffDraftResult | undefined;
|
|
53
70
|
try {
|
|
54
71
|
generatedDraft = await runWithLoader(
|
|
55
72
|
ctx,
|
|
56
73
|
"Generating handoff draft...",
|
|
57
|
-
async (signal: AbortSignal) => generateHandoffDraft(ctx, goal, signal),
|
|
74
|
+
async (signal: AbortSignal) => generateHandoffDraft(ctx, parsedArgs.goal, signal),
|
|
58
75
|
);
|
|
59
76
|
} catch (error) {
|
|
60
77
|
ctx.ui.notify(formatHandoffError(error), "error");
|
|
@@ -72,26 +89,57 @@ export default function sessionHandoffExtension(pi: ExtensionAPI): void {
|
|
|
72
89
|
return;
|
|
73
90
|
}
|
|
74
91
|
|
|
75
|
-
const
|
|
92
|
+
const parentSessionFile = ctx.sessionManager.getSessionFile();
|
|
93
|
+
if (!parentSessionFile) {
|
|
94
|
+
ctx.ui.notify("Handoff requires a persisted current session.", "error");
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
76
97
|
|
|
77
98
|
const handoffMetadata = createHandoffSessionMetadata(
|
|
78
|
-
goal,
|
|
99
|
+
parsedArgs.goal,
|
|
79
100
|
generatedDraft.context.nextTask,
|
|
80
101
|
approvedDraft,
|
|
81
102
|
);
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
103
|
+
const createdSession = createHandoffSession({
|
|
104
|
+
cwd: ctx.cwd,
|
|
105
|
+
sessionDir: ctx.sessionManager.getSessionDir(),
|
|
106
|
+
parentSessionFile,
|
|
107
|
+
});
|
|
108
|
+
const bootstrapValue = encodeHandoffBootstrap(
|
|
109
|
+
createHandoffBootstrap(createdSession.sessionId, handoffMetadata),
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
if (parsedArgs.splitDirection) {
|
|
113
|
+
const launchResult = await launchSplitHandoffSession(pi, {
|
|
114
|
+
cwd: ctx.cwd,
|
|
115
|
+
sessionDir: ctx.sessionManager.getSessionDir(),
|
|
116
|
+
direction: parsedArgs.splitDirection,
|
|
117
|
+
sessionId: createdSession.sessionId,
|
|
118
|
+
bootstrapValue,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
if (!launchResult.success) {
|
|
122
|
+
ctx.ui.notify(
|
|
123
|
+
`${launchResult.error} Created handoff session ${createdSession.sessionId}; start it manually with: ${buildPiResumeCommand(ctx.sessionManager.getSessionDir(), createdSession.sessionId, bootstrapValue)}`,
|
|
124
|
+
"error",
|
|
125
|
+
);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
88
128
|
|
|
89
|
-
|
|
90
|
-
ctx.ui.notify("New session cancelled", "info");
|
|
129
|
+
ctx.ui.notify(`Handoff started in a new pane (${parsedArgs.splitDirection}).`, "info");
|
|
91
130
|
return;
|
|
92
131
|
}
|
|
93
132
|
|
|
94
|
-
|
|
133
|
+
const switchResult = await ctx.switchSession(createdSession.sessionFile, {
|
|
134
|
+
withSession: async (nextCtx) => {
|
|
135
|
+
await nextCtx.sendUserMessage(approvedDraft);
|
|
136
|
+
nextCtx.ui.notify("Handoff started in a new session.", "info");
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
if (switchResult.cancelled) {
|
|
141
|
+
ctx.ui.notify("Session switch cancelled", "info");
|
|
142
|
+
}
|
|
95
143
|
},
|
|
96
144
|
});
|
|
97
145
|
|
|
@@ -116,24 +164,48 @@ export default function sessionHandoffExtension(pi: ExtensionAPI): void {
|
|
|
116
164
|
});
|
|
117
165
|
|
|
118
166
|
pi.on("session_start", async (_event, ctx) => {
|
|
119
|
-
const
|
|
120
|
-
|
|
167
|
+
const encodedBootstrap = process.env[HANDOFF_BOOTSTRAP_ENV];
|
|
168
|
+
if (!encodedBootstrap) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
121
171
|
|
|
122
|
-
|
|
172
|
+
const bootstrap = parseHandoffBootstrap(encodedBootstrap);
|
|
173
|
+
if (!bootstrap) {
|
|
174
|
+
delete process.env[HANDOFF_BOOTSTRAP_ENV];
|
|
123
175
|
return;
|
|
124
176
|
}
|
|
125
177
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
178
|
+
if (bootstrap.sessionId !== ctx.sessionManager.getSessionId()) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
const entries = ctx.sessionManager.getEntries();
|
|
184
|
+
if (hasUserMessages(entries)) {
|
|
185
|
+
if (ctx.hasUI) {
|
|
186
|
+
ctx.ui.notify(HANDOFF_STALE_SESSION_MESSAGE, "error");
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (!getHandoffMetadataFromEntries(entries)) {
|
|
192
|
+
pi.appendEntry(
|
|
193
|
+
HANDOFF_METADATA_CUSTOM_TYPE,
|
|
194
|
+
createHandoffSessionMetadata(bootstrap.goal, bootstrap.nextTask, bootstrap.initialPrompt),
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
pi.sendUserMessage(bootstrap.initialPrompt);
|
|
199
|
+
} finally {
|
|
200
|
+
delete process.env[HANDOFF_BOOTSTRAP_ENV];
|
|
201
|
+
}
|
|
131
202
|
});
|
|
132
203
|
|
|
133
|
-
pi.on("before_agent_start", async () => {
|
|
204
|
+
pi.on("before_agent_start", async (event) => {
|
|
134
205
|
return {
|
|
135
206
|
systemPrompt:
|
|
136
|
-
|
|
207
|
+
event.systemPrompt +
|
|
208
|
+
"\n\nWhen the user references @session:<uuid>, treat it as a session token. If you call session_ask, pass only the UUID value, not the @session: prefix.",
|
|
137
209
|
};
|
|
138
210
|
});
|
|
139
211
|
}
|
|
@@ -201,3 +273,56 @@ function formatHandoffError(error: unknown): string {
|
|
|
201
273
|
|
|
202
274
|
return "Handoff generation failed.";
|
|
203
275
|
}
|
|
276
|
+
|
|
277
|
+
function parseHandoffCommandArgs(
|
|
278
|
+
args: string,
|
|
279
|
+
):
|
|
280
|
+
| { kind: "ok"; goal: string; splitDirection?: HandoffSplitDirection | undefined }
|
|
281
|
+
| { kind: "error"; message: string } {
|
|
282
|
+
const tokens = args
|
|
283
|
+
.trim()
|
|
284
|
+
.split(/\s+/)
|
|
285
|
+
.filter((token) => token.length > 0);
|
|
286
|
+
|
|
287
|
+
if (tokens.length === 0) {
|
|
288
|
+
return { kind: "error", message: HANDOFF_USAGE };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const directionFlags = new Map<string, HandoffSplitDirection>([
|
|
292
|
+
["--left", "left"],
|
|
293
|
+
["--right", "right"],
|
|
294
|
+
["--up", "up"],
|
|
295
|
+
["--down", "down"],
|
|
296
|
+
]);
|
|
297
|
+
|
|
298
|
+
let splitDirection: HandoffSplitDirection | undefined;
|
|
299
|
+
const goalTokens: string[] = [];
|
|
300
|
+
|
|
301
|
+
for (const token of tokens) {
|
|
302
|
+
const direction = directionFlags.get(token);
|
|
303
|
+
if (!direction) {
|
|
304
|
+
goalTokens.push(token);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (splitDirection) {
|
|
309
|
+
return {
|
|
310
|
+
kind: "error",
|
|
311
|
+
message: "Use only one split flag: --left, --right, --up, or --down.",
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
splitDirection = direction;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const goal = goalTokens.join(" ").trim();
|
|
319
|
+
if (!goal) {
|
|
320
|
+
return { kind: "error", message: HANDOFF_USAGE };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
kind: "ok",
|
|
325
|
+
goal,
|
|
326
|
+
splitDirection,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { HANDOFF_BOOTSTRAP_ENV, parseHandoffBootstrap } from "./session-handoff/metadata.js";
|
|
2
3
|
import { createSessionHookController } from "./session-search/hooks.js";
|
|
3
4
|
import { loadSettings } from "./shared/settings.js";
|
|
4
5
|
|
|
@@ -18,7 +19,12 @@ export default function sessionHooksExtension(pi: ExtensionAPI): void {
|
|
|
18
19
|
switch (reason) {
|
|
19
20
|
case "new":
|
|
20
21
|
case "resume":
|
|
21
|
-
await controller.handleSessionSwitch(
|
|
22
|
+
await controller.handleSessionSwitch(
|
|
23
|
+
previousSessionFile,
|
|
24
|
+
sessionFile,
|
|
25
|
+
ctx.cwd,
|
|
26
|
+
getSessionStartOrigin(ctx),
|
|
27
|
+
);
|
|
22
28
|
break;
|
|
23
29
|
case "fork":
|
|
24
30
|
await controller.handleSessionFork(previousSessionFile, sessionFile, ctx.cwd);
|
|
@@ -53,3 +59,17 @@ export default function sessionHooksExtension(pi: ExtensionAPI): void {
|
|
|
53
59
|
await controller.handleSessionShutdown(ctx.sessionManager.getSessionFile(), ctx.cwd);
|
|
54
60
|
});
|
|
55
61
|
}
|
|
62
|
+
|
|
63
|
+
function getSessionStartOrigin(ctx: ExtensionContext): "handoff" | undefined {
|
|
64
|
+
const encodedBootstrap = process.env[HANDOFF_BOOTSTRAP_ENV];
|
|
65
|
+
if (!encodedBootstrap) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const bootstrap = parseHandoffBootstrap(encodedBootstrap);
|
|
70
|
+
if (!bootstrap) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return bootstrap.sessionId === ctx.sessionManager.getSessionId() ? "handoff" : undefined;
|
|
75
|
+
}
|
|
@@ -157,7 +157,7 @@ export function extractSessionRecord(sessionPath: string): ExtractedSessionRecor
|
|
|
157
157
|
}
|
|
158
158
|
case "custom": {
|
|
159
159
|
const nextHandoffMetadata = extractHandoffMetadata(entry, entryTs);
|
|
160
|
-
if (nextHandoffMetadata) {
|
|
160
|
+
if (nextHandoffMetadata && !durableHandoffMetadata) {
|
|
161
161
|
durableHandoffMetadata = nextHandoffMetadata;
|
|
162
162
|
}
|
|
163
163
|
continue;
|
|
@@ -282,7 +282,7 @@ function findDurableHandoffMetadata(
|
|
|
282
282
|
}
|
|
283
283
|
|
|
284
284
|
const parsed = extractHandoffMetadata(entry, getEntryTimestamp(entry, fallbackTs));
|
|
285
|
-
if (parsed) {
|
|
285
|
+
if (parsed && !durableHandoffMetadata) {
|
|
286
286
|
durableHandoffMetadata = parsed;
|
|
287
287
|
}
|
|
288
288
|
}
|
|
@@ -22,6 +22,9 @@ import {
|
|
|
22
22
|
import { type ExtractedSessionRecord, extractSessionRecord } from "./extract.js";
|
|
23
23
|
|
|
24
24
|
const TOOL_RESULT_TEXT_LIMIT = 500;
|
|
25
|
+
const HOOK_INDEX_WRITE_TIMEOUT_MS = 500;
|
|
26
|
+
const HOOK_INDEX_WRITE_MAX_ATTEMPTS = 4;
|
|
27
|
+
const HOOK_INDEX_WRITE_RETRY_DELAYS_MS = [150, 400, 1000];
|
|
25
28
|
|
|
26
29
|
type TrackedToolName = "read" | "edit" | "write";
|
|
27
30
|
|
|
@@ -96,15 +99,24 @@ export function createSessionHookController(options: { indexPath: string }): Ses
|
|
|
96
99
|
return syncAttachedSession(indexPath, state, "session_start");
|
|
97
100
|
},
|
|
98
101
|
async handleSessionSwitch(previousSessionFile, sessionFile, cwd, sessionOrigin) {
|
|
99
|
-
const previousSynced = syncSessionFile(
|
|
102
|
+
const previousSynced = await syncSessionFile(
|
|
103
|
+
indexPath,
|
|
104
|
+
previousSessionFile,
|
|
105
|
+
"session_switch",
|
|
106
|
+
);
|
|
100
107
|
attachSession(state, sessionFile, cwd);
|
|
101
|
-
const currentSynced = syncAttachedSession(
|
|
108
|
+
const currentSynced = await syncAttachedSession(
|
|
109
|
+
indexPath,
|
|
110
|
+
state,
|
|
111
|
+
"session_switch",
|
|
112
|
+
sessionOrigin,
|
|
113
|
+
);
|
|
102
114
|
return previousSynced || currentSynced;
|
|
103
115
|
},
|
|
104
116
|
async handleSessionFork(previousSessionFile, sessionFile, cwd) {
|
|
105
|
-
const previousSynced = syncSessionFile(indexPath, previousSessionFile, "session_fork");
|
|
117
|
+
const previousSynced = await syncSessionFile(indexPath, previousSessionFile, "session_fork");
|
|
106
118
|
attachSession(state, sessionFile, cwd);
|
|
107
|
-
const currentSynced = syncAttachedSession(indexPath, state, "session_fork", "fork");
|
|
119
|
+
const currentSynced = await syncAttachedSession(indexPath, state, "session_fork", "fork");
|
|
108
120
|
return previousSynced || currentSynced;
|
|
109
121
|
},
|
|
110
122
|
handleToolCall(event, sessionFile, cwd) {
|
|
@@ -131,9 +143,11 @@ export function createSessionHookController(options: { indexPath: string }): Ses
|
|
|
131
143
|
},
|
|
132
144
|
async handleTurnEnd(sessionFile, cwd) {
|
|
133
145
|
attachSession(state, sessionFile, cwd);
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
146
|
+
try {
|
|
147
|
+
return await syncAttachedSession(indexPath, state, "turn_end");
|
|
148
|
+
} finally {
|
|
149
|
+
clearTurnState(state);
|
|
150
|
+
}
|
|
137
151
|
},
|
|
138
152
|
async handleSessionTree(sessionFile, cwd) {
|
|
139
153
|
attachSession(state, sessionFile, cwd);
|
|
@@ -145,11 +159,13 @@ export function createSessionHookController(options: { indexPath: string }): Ses
|
|
|
145
159
|
},
|
|
146
160
|
async handleSessionShutdown(sessionFile, cwd) {
|
|
147
161
|
attachSession(state, sessionFile, cwd);
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
162
|
+
try {
|
|
163
|
+
return await syncAttachedSession(indexPath, state, "session_shutdown");
|
|
164
|
+
} finally {
|
|
165
|
+
clearTurnState(state);
|
|
166
|
+
state.currentSessionFile = undefined;
|
|
167
|
+
state.currentCwd = undefined;
|
|
168
|
+
}
|
|
153
169
|
},
|
|
154
170
|
};
|
|
155
171
|
}
|
|
@@ -166,22 +182,66 @@ function attachSession(
|
|
|
166
182
|
state.currentCwd = cwd;
|
|
167
183
|
}
|
|
168
184
|
|
|
185
|
+
async function retryIndexWrite<T>(action: () => T): Promise<T> {
|
|
186
|
+
for (let attempt = 1; attempt <= HOOK_INDEX_WRITE_MAX_ATTEMPTS; attempt += 1) {
|
|
187
|
+
try {
|
|
188
|
+
return action();
|
|
189
|
+
} catch (error) {
|
|
190
|
+
if (!isRetryableSqliteLock(error) || attempt >= HOOK_INDEX_WRITE_MAX_ATTEMPTS) {
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
await sleep(getRetryDelayMs(attempt));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return action();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function getRetryDelayMs(attempt: number): number {
|
|
202
|
+
return (
|
|
203
|
+
HOOK_INDEX_WRITE_RETRY_DELAYS_MS[attempt - 1] ??
|
|
204
|
+
HOOK_INDEX_WRITE_RETRY_DELAYS_MS[HOOK_INDEX_WRITE_RETRY_DELAYS_MS.length - 1] ??
|
|
205
|
+
0
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function isRetryableSqliteLock(error: unknown): boolean {
|
|
210
|
+
if (!(error instanceof Error)) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const code = "code" in error && typeof error.code === "string" ? error.code : undefined;
|
|
215
|
+
const message = error.message.toLowerCase();
|
|
216
|
+
return (
|
|
217
|
+
code === "SQLITE_BUSY" ||
|
|
218
|
+
code === "SQLITE_LOCKED" ||
|
|
219
|
+
code?.startsWith("SQLITE_BUSY_") === true ||
|
|
220
|
+
code?.startsWith("SQLITE_LOCKED_") === true ||
|
|
221
|
+
message.includes("database is locked")
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function sleep(ms: number): Promise<void> {
|
|
226
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
227
|
+
}
|
|
228
|
+
|
|
169
229
|
function syncAttachedSession(
|
|
170
230
|
indexPath: string,
|
|
171
231
|
state: SessionHookState,
|
|
172
232
|
eventType: string,
|
|
173
233
|
sessionOrigin?: SessionOrigin,
|
|
174
|
-
): boolean {
|
|
234
|
+
): Promise<boolean> {
|
|
175
235
|
return syncSessionFile(indexPath, state.currentSessionFile, eventType, state, sessionOrigin);
|
|
176
236
|
}
|
|
177
237
|
|
|
178
|
-
function syncSessionFile(
|
|
238
|
+
async function syncSessionFile(
|
|
179
239
|
indexPath: string,
|
|
180
240
|
sessionFile: string | undefined,
|
|
181
241
|
eventType: string,
|
|
182
242
|
state?: SessionHookState,
|
|
183
243
|
sessionOrigin?: SessionOrigin,
|
|
184
|
-
): boolean {
|
|
244
|
+
): Promise<boolean> {
|
|
185
245
|
if (!sessionFile || !existsSync(sessionFile)) {
|
|
186
246
|
return false;
|
|
187
247
|
}
|
|
@@ -196,31 +256,36 @@ function syncSessionFile(
|
|
|
196
256
|
return false;
|
|
197
257
|
}
|
|
198
258
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
259
|
+
await retryIndexWrite(() => {
|
|
260
|
+
const db = openIndexDatabase(indexPath, {
|
|
261
|
+
create: false,
|
|
262
|
+
timeoutMs: HOOK_INDEX_WRITE_TIMEOUT_MS,
|
|
263
|
+
});
|
|
264
|
+
try {
|
|
265
|
+
db.transaction(() => {
|
|
266
|
+
const existingSession = getSessionById(db, extracted.sessionId);
|
|
267
|
+
const sessionRow = mergeSessionLineage(extracted, existingSession, sessionOrigin);
|
|
268
|
+
upsertSession(db, sessionRow, "hook");
|
|
269
|
+
if (shouldRefreshLineageRelations(existingSession, sessionRow)) {
|
|
270
|
+
rebuildSessionLineageRelations(db);
|
|
271
|
+
}
|
|
272
|
+
clearSessionIndexedData(db, extracted.sessionId);
|
|
273
|
+
|
|
274
|
+
for (const chunk of extracted.chunks) {
|
|
275
|
+
insertTextChunk(db, { sessionId: extracted.sessionId, ...chunk });
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
for (const fileTouch of extracted.fileTouches) {
|
|
279
|
+
insertSessionFileTouch(db, { sessionId: extracted.sessionId, ...fileTouch });
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
setMetadata(db, "hook_updated_at", new Date().toISOString());
|
|
283
|
+
setMetadata(db, "hook_last_event", eventType);
|
|
284
|
+
})();
|
|
285
|
+
} finally {
|
|
286
|
+
db.close();
|
|
287
|
+
}
|
|
288
|
+
});
|
|
224
289
|
|
|
225
290
|
if (state) {
|
|
226
291
|
state.lastFlushedSessionFile = sessionFile;
|
|
@@ -26,10 +26,15 @@ export function createTempIndexPath(finalPath: string): string {
|
|
|
26
26
|
|
|
27
27
|
export function openIndexDatabase(
|
|
28
28
|
dbPath: string,
|
|
29
|
-
options?: { create?: boolean },
|
|
29
|
+
options?: { create?: boolean; timeoutMs?: number },
|
|
30
30
|
): SessionIndexDatabase {
|
|
31
31
|
const create = options?.create ?? true;
|
|
32
|
-
const db = new Database(
|
|
32
|
+
const db = new Database(
|
|
33
|
+
dbPath,
|
|
34
|
+
options?.timeoutMs === undefined
|
|
35
|
+
? { fileMustExist: !create }
|
|
36
|
+
: { fileMustExist: !create, timeout: options.timeoutMs },
|
|
37
|
+
);
|
|
33
38
|
db.pragma("journal_mode = WAL");
|
|
34
39
|
db.pragma("synchronous = NORMAL");
|
|
35
40
|
db.pragma("foreign_keys = ON");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import os from "node:os";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { SettingsManager } from "@mariozechner/pi-coding-agent";
|
|
4
4
|
import type { KeyId } from "@mariozechner/pi-tui";
|
|
5
5
|
import { type Static, Type } from "@sinclair/typebox";
|
|
6
6
|
import { parseTypeBoxValue } from "./typebox.js";
|
|
@@ -114,7 +114,7 @@ function parseModelReference(value: string | undefined): ModelReference | undefi
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
function loadSessionFileSettings(): SessionFileSettings {
|
|
117
|
-
const globalSettings = SettingsManager.create(
|
|
117
|
+
const globalSettings = SettingsManager.create(process.cwd()).getGlobalSettings();
|
|
118
118
|
const parsed = parseTypeBoxValue(ROOT_SETTINGS_SCHEMA, globalSettings, "Invalid settings");
|
|
119
119
|
return parsed.sessions ?? {};
|
|
120
120
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-sessions",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Pi session search, ask, handoff, auto-titling, and indexing tools",
|
|
6
6
|
"license": "MIT",
|
|
@@ -38,27 +38,28 @@
|
|
|
38
38
|
"prepare": "husky"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@biomejs/biome": "^2.4.
|
|
42
|
-
"@mariozechner/pi-ai": "^0.
|
|
43
|
-
"@mariozechner/pi-coding-agent": "^0.
|
|
44
|
-
"@mariozechner/pi-tui": "^0.
|
|
41
|
+
"@biomejs/biome": "^2.4.12",
|
|
42
|
+
"@mariozechner/pi-ai": "^0.70.2",
|
|
43
|
+
"@mariozechner/pi-coding-agent": "^0.70.2",
|
|
44
|
+
"@mariozechner/pi-tui": "^0.70.2",
|
|
45
|
+
"@sinclair/typebox": "^0.34.49",
|
|
45
46
|
"@types/better-sqlite3": "^7.6.13",
|
|
46
47
|
"@types/node": "^25.6.0",
|
|
47
48
|
"husky": "^9.1.7",
|
|
48
49
|
"lint-staged": "^16.4.0",
|
|
49
|
-
"typescript": "^6.0.
|
|
50
|
+
"typescript": "^6.0.3",
|
|
50
51
|
"vitest": "^4.1.4"
|
|
51
52
|
},
|
|
52
|
-
"dependencies": {
|
|
53
|
-
"better-sqlite3": "^12.8.0"
|
|
54
|
-
},
|
|
55
53
|
"peerDependencies": {
|
|
56
|
-
"@mariozechner/pi-ai": ">=0.
|
|
57
|
-
"@mariozechner/pi-coding-agent": ">=0.
|
|
58
|
-
"@mariozechner/pi-tui": ">=0.
|
|
59
|
-
"@sinclair/typebox": ">=0.34.
|
|
54
|
+
"@mariozechner/pi-ai": ">=0.70.2",
|
|
55
|
+
"@mariozechner/pi-coding-agent": ">=0.70.2",
|
|
56
|
+
"@mariozechner/pi-tui": ">=0.70.2",
|
|
57
|
+
"@sinclair/typebox": ">=0.34.41"
|
|
60
58
|
},
|
|
61
59
|
"lint-staged": {
|
|
62
60
|
"*.{js,cjs,mjs,jsx,ts,tsx,json,jsonc}": "biome check --write --no-errors-on-unmatched"
|
|
61
|
+
},
|
|
62
|
+
"dependencies": {
|
|
63
|
+
"better-sqlite3": "^12.9.0"
|
|
63
64
|
}
|
|
64
65
|
}
|