pi-sessions 0.1.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/LICENSE +21 -0
- package/README.md +173 -0
- package/extensions/session-ask.ts +297 -0
- package/extensions/session-auto-title/command.ts +109 -0
- package/extensions/session-auto-title/context.ts +41 -0
- package/extensions/session-auto-title/controller.ts +298 -0
- package/extensions/session-auto-title/model.ts +61 -0
- package/extensions/session-auto-title/prompt.ts +54 -0
- package/extensions/session-auto-title/retitle.ts +641 -0
- package/extensions/session-auto-title/state.ts +69 -0
- package/extensions/session-auto-title/wizard.ts +583 -0
- package/extensions/session-auto-title.ts +209 -0
- package/extensions/session-handoff/extract.ts +278 -0
- package/extensions/session-handoff/metadata.ts +96 -0
- package/extensions/session-handoff/picker.ts +394 -0
- package/extensions/session-handoff/query.ts +318 -0
- package/extensions/session-handoff/refs.ts +151 -0
- package/extensions/session-handoff/review.ts +268 -0
- package/extensions/session-handoff.ts +203 -0
- package/extensions/session-hooks.ts +55 -0
- package/extensions/session-index.ts +159 -0
- package/extensions/session-search/extract.ts +997 -0
- package/extensions/session-search/hooks.ts +350 -0
- package/extensions/session-search/normalize.ts +170 -0
- package/extensions/session-search/reindex.ts +93 -0
- package/extensions/session-search.ts +390 -0
- package/extensions/shared/search-snippet.ts +40 -0
- package/extensions/shared/session-index/common.ts +222 -0
- package/extensions/shared/session-index/index.ts +5 -0
- package/extensions/shared/session-index/lineage.ts +417 -0
- package/extensions/shared/session-index/schema.ts +178 -0
- package/extensions/shared/session-index/search.ts +688 -0
- package/extensions/shared/session-index/store.ts +173 -0
- package/extensions/shared/session-ui.ts +15 -0
- package/extensions/shared/settings.ts +141 -0
- package/extensions/shared/time.ts +38 -0
- package/extensions/shared/typebox.ts +61 -0
- package/images/handoff.png +0 -0
- package/images/session-title.png +0 -0
- package/images/session_ask.png +0 -0
- package/images/session_picker.png +0 -0
- package/images/session_search.png +0 -0
- package/package.json +64 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import {
|
|
2
|
+
compactSessionId,
|
|
3
|
+
INDEX_SCHEMA_VERSION,
|
|
4
|
+
type SessionFileTouchRow,
|
|
5
|
+
type SessionIndexDatabase,
|
|
6
|
+
type SessionRow,
|
|
7
|
+
type SessionTextChunkRow,
|
|
8
|
+
} from "./common.js";
|
|
9
|
+
|
|
10
|
+
function sessionRowBindings(row: SessionRow, indexSource: string) {
|
|
11
|
+
return [
|
|
12
|
+
row.sessionId,
|
|
13
|
+
row.sessionPath,
|
|
14
|
+
row.sessionName,
|
|
15
|
+
row.firstUserPrompt ?? null,
|
|
16
|
+
row.cwd,
|
|
17
|
+
JSON.stringify(row.repoRoots),
|
|
18
|
+
row.startedAt,
|
|
19
|
+
row.modifiedAt,
|
|
20
|
+
row.messageCount,
|
|
21
|
+
row.entryCount,
|
|
22
|
+
row.parentSessionPath ?? null,
|
|
23
|
+
row.parentSessionId ?? null,
|
|
24
|
+
row.sessionOrigin ?? null,
|
|
25
|
+
row.handoffGoal ?? null,
|
|
26
|
+
row.handoffNextTask ?? null,
|
|
27
|
+
INDEX_SCHEMA_VERSION,
|
|
28
|
+
new Date().toISOString(),
|
|
29
|
+
indexSource,
|
|
30
|
+
];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function insertSession(
|
|
34
|
+
db: SessionIndexDatabase,
|
|
35
|
+
row: SessionRow,
|
|
36
|
+
indexSource: string,
|
|
37
|
+
): void {
|
|
38
|
+
db.prepare(
|
|
39
|
+
`
|
|
40
|
+
INSERT INTO sessions(
|
|
41
|
+
session_id, session_path, session_name, first_user_prompt, cwd, repo_roots_json,
|
|
42
|
+
created_ts, modified_ts, message_count, entry_count,
|
|
43
|
+
parent_session_path, parent_session_id, session_origin,
|
|
44
|
+
handoff_goal, handoff_next_task,
|
|
45
|
+
index_version, indexed_at_ts, index_source
|
|
46
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
47
|
+
`,
|
|
48
|
+
).run(...sessionRowBindings(row, indexSource));
|
|
49
|
+
insertSessionIdChunk(db, row);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function upsertSession(
|
|
53
|
+
db: SessionIndexDatabase,
|
|
54
|
+
row: SessionRow,
|
|
55
|
+
indexSource: string,
|
|
56
|
+
): void {
|
|
57
|
+
db.prepare(
|
|
58
|
+
`
|
|
59
|
+
INSERT INTO sessions(
|
|
60
|
+
session_id, session_path, session_name, first_user_prompt, cwd, repo_roots_json,
|
|
61
|
+
created_ts, modified_ts, message_count, entry_count,
|
|
62
|
+
parent_session_path, parent_session_id, session_origin,
|
|
63
|
+
handoff_goal, handoff_next_task,
|
|
64
|
+
index_version, indexed_at_ts, index_source
|
|
65
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
66
|
+
ON CONFLICT(session_id) DO UPDATE SET
|
|
67
|
+
session_path = excluded.session_path,
|
|
68
|
+
session_name = excluded.session_name,
|
|
69
|
+
first_user_prompt = excluded.first_user_prompt,
|
|
70
|
+
cwd = excluded.cwd,
|
|
71
|
+
repo_roots_json = excluded.repo_roots_json,
|
|
72
|
+
created_ts = excluded.created_ts,
|
|
73
|
+
modified_ts = excluded.modified_ts,
|
|
74
|
+
message_count = excluded.message_count,
|
|
75
|
+
entry_count = excluded.entry_count,
|
|
76
|
+
parent_session_path = excluded.parent_session_path,
|
|
77
|
+
parent_session_id = excluded.parent_session_id,
|
|
78
|
+
session_origin = excluded.session_origin,
|
|
79
|
+
handoff_goal = excluded.handoff_goal,
|
|
80
|
+
handoff_next_task = excluded.handoff_next_task,
|
|
81
|
+
index_version = excluded.index_version,
|
|
82
|
+
indexed_at_ts = excluded.indexed_at_ts,
|
|
83
|
+
index_source = excluded.index_source
|
|
84
|
+
`,
|
|
85
|
+
).run(...sessionRowBindings(row, indexSource));
|
|
86
|
+
syncSessionIdChunk(db, row);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function insertSessionIdChunk(db: SessionIndexDatabase, row: SessionRow): void {
|
|
90
|
+
insertTextChunk(db, {
|
|
91
|
+
sessionId: row.sessionId,
|
|
92
|
+
entryType: "session_info",
|
|
93
|
+
ts: row.modifiedAt,
|
|
94
|
+
sourceKind: "session_id",
|
|
95
|
+
text: buildSessionIdSearchText(row.sessionId),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function syncSessionIdChunk(db: SessionIndexDatabase, row: SessionRow): void {
|
|
100
|
+
db.prepare(
|
|
101
|
+
`DELETE FROM session_text_chunks_fts
|
|
102
|
+
WHERE chunk_id IN (
|
|
103
|
+
SELECT CAST(id AS TEXT) FROM session_text_chunks
|
|
104
|
+
WHERE session_id = ? AND source_kind = 'session_id'
|
|
105
|
+
)`,
|
|
106
|
+
).run(row.sessionId);
|
|
107
|
+
|
|
108
|
+
db.prepare(
|
|
109
|
+
`DELETE FROM session_text_chunks WHERE session_id = ? AND source_kind = 'session_id'`,
|
|
110
|
+
).run(row.sessionId);
|
|
111
|
+
|
|
112
|
+
insertSessionIdChunk(db, row);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function buildSessionIdSearchText(sessionId: string): string {
|
|
116
|
+
const compact = compactSessionId(sessionId);
|
|
117
|
+
return compact === sessionId ? sessionId : `${sessionId} ${compact}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function clearSessionIndexedData(db: SessionIndexDatabase, sessionId: string): void {
|
|
121
|
+
db.prepare(`DELETE FROM session_text_chunks_fts WHERE session_id = ?`).run(sessionId);
|
|
122
|
+
db.prepare(`DELETE FROM session_text_chunks WHERE session_id = ?`).run(sessionId);
|
|
123
|
+
db.prepare(`DELETE FROM session_file_touches WHERE session_id = ?`).run(sessionId);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function insertTextChunk(db: SessionIndexDatabase, row: SessionTextChunkRow): void {
|
|
127
|
+
const result = db
|
|
128
|
+
.prepare(
|
|
129
|
+
`
|
|
130
|
+
INSERT INTO session_text_chunks(
|
|
131
|
+
session_id, entry_id, entry_type, role, ts, source_kind, text
|
|
132
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
133
|
+
`,
|
|
134
|
+
)
|
|
135
|
+
.run(
|
|
136
|
+
row.sessionId,
|
|
137
|
+
row.entryId ?? null,
|
|
138
|
+
row.entryType,
|
|
139
|
+
row.role ?? null,
|
|
140
|
+
row.ts,
|
|
141
|
+
row.sourceKind,
|
|
142
|
+
row.text,
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const chunkId = Number(result.lastInsertRowid);
|
|
146
|
+
db.prepare(
|
|
147
|
+
`INSERT INTO session_text_chunks_fts(chunk_id, session_id, text) VALUES (?, ?, ?)`,
|
|
148
|
+
).run(chunkId, row.sessionId, row.text);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function insertSessionFileTouch(db: SessionIndexDatabase, row: SessionFileTouchRow): void {
|
|
152
|
+
db.prepare(
|
|
153
|
+
`
|
|
154
|
+
INSERT INTO session_file_touches(
|
|
155
|
+
session_id, entry_id, op, source, raw_path, abs_path, cwd_rel_path,
|
|
156
|
+
repo_root, repo_rel_path, basename, path_scope, ts
|
|
157
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
158
|
+
`,
|
|
159
|
+
).run(
|
|
160
|
+
row.sessionId,
|
|
161
|
+
row.entryId ?? null,
|
|
162
|
+
row.op,
|
|
163
|
+
row.source,
|
|
164
|
+
row.rawPath,
|
|
165
|
+
row.absPath ?? null,
|
|
166
|
+
row.cwdRelPath ?? null,
|
|
167
|
+
row.repoRoot ?? null,
|
|
168
|
+
row.repoRelPath ?? null,
|
|
169
|
+
row.basename,
|
|
170
|
+
row.pathScope,
|
|
171
|
+
row.ts,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function isExactSessionId(value: string): boolean {
|
|
2
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function shortenSessionId(sessionId: string): string {
|
|
6
|
+
return isExactSessionId(sessionId) ? sessionId.slice(0, 8) : sessionId;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function formatSessionTitleOrShortId(
|
|
10
|
+
sessionName: string | undefined,
|
|
11
|
+
sessionId: string | undefined,
|
|
12
|
+
): string {
|
|
13
|
+
const title = sessionName?.trim();
|
|
14
|
+
return title && title.length > 0 ? title : shortenSessionId(sessionId ?? "");
|
|
15
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { getAgentDir, SettingsManager } from "@mariozechner/pi-coding-agent";
|
|
4
|
+
import type { KeyId } from "@mariozechner/pi-tui";
|
|
5
|
+
import { type Static, Type } from "@sinclair/typebox";
|
|
6
|
+
import { parseTypeBoxValue } from "./typebox.js";
|
|
7
|
+
|
|
8
|
+
export const DEFAULT_AUTO_TITLE_REFRESH_TURNS = 4;
|
|
9
|
+
const SESSION_FILE_SETTINGS_SCHEMA = Type.Object({
|
|
10
|
+
handoff: Type.Optional(
|
|
11
|
+
Type.Object({
|
|
12
|
+
pickerShortcut: Type.Optional(Type.String()),
|
|
13
|
+
}),
|
|
14
|
+
),
|
|
15
|
+
index: Type.Optional(
|
|
16
|
+
Type.Object({
|
|
17
|
+
dir: Type.Optional(Type.String()),
|
|
18
|
+
}),
|
|
19
|
+
),
|
|
20
|
+
autoTitle: Type.Optional(
|
|
21
|
+
Type.Object({
|
|
22
|
+
refreshTurns: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
23
|
+
model: Type.Optional(Type.String()),
|
|
24
|
+
}),
|
|
25
|
+
),
|
|
26
|
+
});
|
|
27
|
+
const ROOT_SETTINGS_SCHEMA = Type.Object({
|
|
28
|
+
sessions: Type.Optional(SESSION_FILE_SETTINGS_SCHEMA),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export class ModelReference {
|
|
32
|
+
constructor(
|
|
33
|
+
readonly provider: string,
|
|
34
|
+
readonly modelId: string,
|
|
35
|
+
) {}
|
|
36
|
+
|
|
37
|
+
toString(): string {
|
|
38
|
+
return `${this.provider}/${this.modelId}`;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface AutoTitleSettings {
|
|
43
|
+
refreshTurns: number;
|
|
44
|
+
model: ModelReference | undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface SessionSettings {
|
|
48
|
+
handoff: {
|
|
49
|
+
pickerShortcut: KeyId;
|
|
50
|
+
};
|
|
51
|
+
index: {
|
|
52
|
+
path: string;
|
|
53
|
+
};
|
|
54
|
+
autoTitle: AutoTitleSettings;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type SessionFileSettings = Static<typeof SESSION_FILE_SETTINGS_SCHEMA>;
|
|
58
|
+
|
|
59
|
+
export function getDefaultIndexDir(): string {
|
|
60
|
+
return path.join(os.homedir(), ".pi", "agent", "pi-sessions");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function getDefaultIndexPath(): string {
|
|
64
|
+
return path.join(getDefaultIndexDir(), "index.sqlite");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function expandHome(rawPath: string): string {
|
|
68
|
+
if (rawPath === "~") {
|
|
69
|
+
return os.homedir();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (rawPath.startsWith(`~${path.sep}`) || rawPath.startsWith("~/")) {
|
|
73
|
+
return path.join(os.homedir(), rawPath.slice(2));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return rawPath;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function normalizeIndexDir(value: string | undefined): string {
|
|
80
|
+
if (value === undefined) {
|
|
81
|
+
return getDefaultIndexDir();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const trimmed = value.trim();
|
|
85
|
+
if (trimmed.length === 0) {
|
|
86
|
+
return getDefaultIndexDir();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const expanded = expandHome(trimmed);
|
|
90
|
+
if (!path.isAbsolute(expanded)) {
|
|
91
|
+
throw new Error('sessions.index.dir must be an absolute path or start with "~/".');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return path.normalize(expanded);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function normalizePickerShortcut(value: string | undefined): KeyId {
|
|
98
|
+
const trimmed = value?.trim();
|
|
99
|
+
return (trimmed ? trimmed : "alt+o") as KeyId;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function parseModelReference(value: string | undefined): ModelReference | undefined {
|
|
103
|
+
const trimmed = value?.trim();
|
|
104
|
+
if (!trimmed) {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const slashIndex = trimmed.indexOf("/");
|
|
109
|
+
if (slashIndex <= 0 || slashIndex === trimmed.length - 1) {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return new ModelReference(trimmed.slice(0, slashIndex), trimmed.slice(slashIndex + 1));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function loadSessionFileSettings(): SessionFileSettings {
|
|
117
|
+
const globalSettings = SettingsManager.create(undefined, getAgentDir()).getGlobalSettings();
|
|
118
|
+
const parsed = parseTypeBoxValue(ROOT_SETTINGS_SCHEMA, globalSettings, "Invalid settings");
|
|
119
|
+
return parsed.sessions ?? {};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function resolveSessionSettings(fileSettings: SessionFileSettings): SessionSettings {
|
|
123
|
+
const indexDir = normalizeIndexDir(fileSettings.index?.dir);
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
handoff: {
|
|
127
|
+
pickerShortcut: normalizePickerShortcut(fileSettings.handoff?.pickerShortcut),
|
|
128
|
+
},
|
|
129
|
+
index: {
|
|
130
|
+
path: path.join(indexDir, "index.sqlite"),
|
|
131
|
+
},
|
|
132
|
+
autoTitle: {
|
|
133
|
+
refreshTurns: fileSettings.autoTitle?.refreshTurns ?? DEFAULT_AUTO_TITLE_REFRESH_TURNS,
|
|
134
|
+
model: parseModelReference(fileSettings.autoTitle?.model),
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function loadSettings(): SessionSettings {
|
|
140
|
+
return resolveSessionSettings(loadSessionFileSettings());
|
|
141
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const RELATIVE_TIME_FORMATTER = new Intl.RelativeTimeFormat("en", {
|
|
2
|
+
numeric: "always",
|
|
3
|
+
style: "narrow",
|
|
4
|
+
});
|
|
5
|
+
|
|
6
|
+
const RELATIVE_TIME_UNITS = [
|
|
7
|
+
["year", 60 * 60 * 24 * 365],
|
|
8
|
+
["month", 60 * 60 * 24 * 30],
|
|
9
|
+
["week", 60 * 60 * 24 * 7],
|
|
10
|
+
["day", 60 * 60 * 24],
|
|
11
|
+
["hour", 60 * 60],
|
|
12
|
+
["minute", 60],
|
|
13
|
+
["second", 1],
|
|
14
|
+
] as const;
|
|
15
|
+
|
|
16
|
+
export function formatCompactRelativeTime(
|
|
17
|
+
value: string | number | Date,
|
|
18
|
+
nowMs: number = Date.now(),
|
|
19
|
+
): string | undefined {
|
|
20
|
+
const targetMs =
|
|
21
|
+
value instanceof Date ? value.getTime() : typeof value === "string" ? Date.parse(value) : value;
|
|
22
|
+
if (Number.isNaN(targetMs)) {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const diffSeconds = Math.round((targetMs - nowMs) / 1000);
|
|
27
|
+
const absDiffSeconds = Math.abs(diffSeconds);
|
|
28
|
+
if (absDiffSeconds < 60) {
|
|
29
|
+
return "now";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
for (const [unit, secondsPerUnit] of RELATIVE_TIME_UNITS) {
|
|
33
|
+
if (absDiffSeconds >= secondsPerUnit || unit === "second") {
|
|
34
|
+
const valueForUnit = Math.sign(diffSeconds) * Math.floor(absDiffSeconds / secondsPerUnit);
|
|
35
|
+
return RELATIVE_TIME_FORMATTER.format(valueForUnit, unit).replace(/ ago$/, "");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Static, TSchema } from "@sinclair/typebox";
|
|
2
|
+
import { Value } from "@sinclair/typebox/value";
|
|
3
|
+
|
|
4
|
+
export function isTypeBoxValue<T extends TSchema>(schema: T, value: unknown): value is Static<T> {
|
|
5
|
+
return Value.Check(schema, value);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function parseTypeBoxValue<T extends TSchema>(
|
|
9
|
+
schema: T,
|
|
10
|
+
value: unknown,
|
|
11
|
+
context: string,
|
|
12
|
+
): Static<T> {
|
|
13
|
+
if (Value.Check(schema, value)) {
|
|
14
|
+
return value as Static<T>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
throw new Error(formatTypeBoxError(schema, value, context));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function safeParseTypeBoxValue<T extends TSchema>(
|
|
21
|
+
schema: T,
|
|
22
|
+
value: unknown,
|
|
23
|
+
): Static<T> | undefined {
|
|
24
|
+
return Value.Check(schema, value) ? (value as Static<T>) : undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function parseTypeBoxRows<T extends TSchema>(
|
|
28
|
+
schema: T,
|
|
29
|
+
value: unknown,
|
|
30
|
+
context: string,
|
|
31
|
+
): Static<T>[] {
|
|
32
|
+
if (!Array.isArray(value)) {
|
|
33
|
+
throw new Error(`${context}: expected an array of rows.`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return value.map((row, index) =>
|
|
37
|
+
parseTypeBoxValue(schema, row, `${context} at row ${index + 1}`),
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function safeParseTypeBoxJson<T extends TSchema>(
|
|
42
|
+
schema: T,
|
|
43
|
+
raw: string,
|
|
44
|
+
): Static<T> | undefined {
|
|
45
|
+
try {
|
|
46
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
47
|
+
return safeParseTypeBoxValue(schema, parsed);
|
|
48
|
+
} catch {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function formatTypeBoxError(schema: TSchema, value: unknown, context: string): string {
|
|
54
|
+
const firstError = Value.Errors(schema, value).First();
|
|
55
|
+
if (!firstError) {
|
|
56
|
+
return `${context}: invalid value.`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const path = firstError.path.length > 0 ? firstError.path : "/";
|
|
60
|
+
return `${context}: ${path} ${firstError.message}`;
|
|
61
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-sessions",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Pi session search, ask, handoff, auto-titling, and indexing tools",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/thurstonsand/pi-sessions.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/thurstonsand/pi-sessions#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/thurstonsand/pi-sessions/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"pi-package"
|
|
17
|
+
],
|
|
18
|
+
"files": [
|
|
19
|
+
"extensions",
|
|
20
|
+
"images"
|
|
21
|
+
],
|
|
22
|
+
"pi": {
|
|
23
|
+
"extensions": [
|
|
24
|
+
"./extensions/session-ask.ts",
|
|
25
|
+
"./extensions/session-auto-title.ts",
|
|
26
|
+
"./extensions/session-handoff.ts",
|
|
27
|
+
"./extensions/session-hooks.ts",
|
|
28
|
+
"./extensions/session-index.ts",
|
|
29
|
+
"./extensions/session-search.ts"
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"lint": "biome check .",
|
|
34
|
+
"format": "biome format --write .",
|
|
35
|
+
"typecheck": "tsc --noEmit",
|
|
36
|
+
"test": "vitest run",
|
|
37
|
+
"check": "biome check . && tsc --noEmit && vitest run",
|
|
38
|
+
"prepare": "husky"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@biomejs/biome": "^2.4.11",
|
|
42
|
+
"@mariozechner/pi-ai": "^0.66.1",
|
|
43
|
+
"@mariozechner/pi-coding-agent": "^0.66.1",
|
|
44
|
+
"@mariozechner/pi-tui": "^0.66.1",
|
|
45
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
46
|
+
"@types/node": "^25.6.0",
|
|
47
|
+
"husky": "^9.1.7",
|
|
48
|
+
"lint-staged": "^16.4.0",
|
|
49
|
+
"typescript": "^6.0.2",
|
|
50
|
+
"vitest": "^4.1.4"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"better-sqlite3": "^12.8.0"
|
|
54
|
+
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"@mariozechner/pi-ai": ">=0.66.1",
|
|
57
|
+
"@mariozechner/pi-coding-agent": ">=0.66.1",
|
|
58
|
+
"@mariozechner/pi-tui": ">=0.66.1",
|
|
59
|
+
"@sinclair/typebox": ">=0.34.49"
|
|
60
|
+
},
|
|
61
|
+
"lint-staged": {
|
|
62
|
+
"*.{js,cjs,mjs,jsx,ts,tsx,json,jsonc}": "biome check --write --no-errors-on-unmatched"
|
|
63
|
+
}
|
|
64
|
+
}
|