@daniel156161/prism 0.2.90 → 0.2.92
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/node_modules/@earendil-works/pi-coding-agent/dist/cli.js +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/config.js +6 -11
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session-services.js +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session.js +5 -0
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/auth-storage.js +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/extensions/loader.js +5 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/prism-session-db.js +128 -0
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/project-trust.js +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/sdk.js +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/session-manager.js +39 -37
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/system-prompt.js +20 -16
- package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/session-selector.js +4 -0
- package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js +233 -12
- package/node_modules/@earendil-works/pi-tui/dist/autocomplete.js +1 -1
- package/package.json +1 -3
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { APP_NAME } from "./config.js";
|
|
9
9
|
import { configureHttpDispatcher } from "./core/http-dispatcher.js";
|
|
10
10
|
import { main } from "./main.js";
|
|
11
|
-
process.title =
|
|
11
|
+
process.title = "▲";
|
|
12
12
|
process.env.PI_CODING_AGENT = "true";
|
|
13
13
|
process.env.AI_AGENT = "pi";
|
|
14
14
|
process.emitWarning = (() => { });
|
|
@@ -321,10 +321,9 @@ export function getThemesDir() {
|
|
|
321
321
|
if (isBunBinary) {
|
|
322
322
|
return join(getPackageDir(), "theme");
|
|
323
323
|
}
|
|
324
|
-
//
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
return join(packageDir, srcOrDist, "modes", "interactive", "theme");
|
|
324
|
+
// Prism: use __dirname so built-in themes resolve from the actual dist/,
|
|
325
|
+
// not PI_PACKAGE_DIR which may point to a config-only dir without dist/.
|
|
326
|
+
return join(__dirname, "modes", "interactive", "theme");
|
|
328
327
|
}
|
|
329
328
|
/**
|
|
330
329
|
* Get path to HTML export template directory (shipped with package)
|
|
@@ -336,9 +335,7 @@ export function getExportTemplateDir() {
|
|
|
336
335
|
if (isBunBinary) {
|
|
337
336
|
return join(getPackageDir(), "export-html");
|
|
338
337
|
}
|
|
339
|
-
|
|
340
|
-
const srcOrDist = existsSync(join(packageDir, "src")) ? "src" : "dist";
|
|
341
|
-
return join(packageDir, srcOrDist, "core", "export-html");
|
|
338
|
+
return join(__dirname, "core", "export-html");
|
|
342
339
|
}
|
|
343
340
|
/** Get path to package.json */
|
|
344
341
|
export function getPackageJsonPath() {
|
|
@@ -370,9 +367,7 @@ export function getInteractiveAssetsDir() {
|
|
|
370
367
|
if (isBunBinary) {
|
|
371
368
|
return join(getPackageDir(), "assets");
|
|
372
369
|
}
|
|
373
|
-
|
|
374
|
-
const srcOrDist = existsSync(join(packageDir, "src")) ? "src" : "dist";
|
|
375
|
-
return join(packageDir, srcOrDist, "modes", "interactive", "assets");
|
|
370
|
+
return join(__dirname, "modes", "interactive", "assets");
|
|
376
371
|
}
|
|
377
372
|
/** Get path to a bundled interactive asset */
|
|
378
373
|
export function getBundledInteractiveAssetPath(name) {
|
|
@@ -426,7 +421,7 @@ export function getModelsPath() {
|
|
|
426
421
|
}
|
|
427
422
|
/** Get path to auth.json */
|
|
428
423
|
export function getAuthPath() {
|
|
429
|
-
return join(getAgentDir(), "auth.json");
|
|
424
|
+
return process.env.PRISM_AUTH_PATH || process.env.PI_AUTH_PATH || join(getAgentDir(), "auth.json");
|
|
430
425
|
}
|
|
431
426
|
/** Get path to settings.json */
|
|
432
427
|
export function getSettingsPath() {
|
|
@@ -55,7 +55,7 @@ export async function createAgentSessionServices(options) {
|
|
|
55
55
|
const agentDir = options.agentDir ? resolvePath(options.agentDir) : getAgentDir();
|
|
56
56
|
const modelRuntime = options.modelRuntime ??
|
|
57
57
|
(await ModelRuntime.create({
|
|
58
|
-
authPath: join(agentDir, "auth.json"),
|
|
58
|
+
authPath: process.env.PRISM_AUTH_PATH || process.env.PI_AUTH_PATH || join(agentDir, "auth.json"),
|
|
59
59
|
modelsPath: join(agentDir, "models.json"),
|
|
60
60
|
signal: options.modelRuntimeSignal,
|
|
61
61
|
}));
|
|
@@ -2084,6 +2084,11 @@ export class AgentSession {
|
|
|
2084
2084
|
// Context overflow is handled by compaction, not retry.
|
|
2085
2085
|
if (isContextOverflow(message, this.model?.contextWindow ?? 0))
|
|
2086
2086
|
return false;
|
|
2087
|
+
// Prism: provider account/quota usage limits need a user decision (wait, switch model, or cancel).
|
|
2088
|
+
// Do not auto-retry them with the generic exponential backoff, because provider reset
|
|
2089
|
+
// windows are often much longer than the retry delay.
|
|
2090
|
+
if (/GoUsageLimitError|FreeUsageLimitError|Monthly usage limit reached|available balance|insufficient_quota|out of budget|quota exceeded|billing|usage[_\s-]*limit|usage_limit_reached/i.test(message.errorMessage || ""))
|
|
2091
|
+
return false;
|
|
2087
2092
|
return isRetryableAssistantError(message);
|
|
2088
2093
|
}
|
|
2089
2094
|
/**
|
|
@@ -14,7 +14,7 @@ const AUTH_FILE_WRITE_OPTIONS = { encoding: "utf-8", mode: 0o600 };
|
|
|
14
14
|
let sharedAuthFileReadState;
|
|
15
15
|
export class FileAuthStorageBackend {
|
|
16
16
|
authPath;
|
|
17
|
-
constructor(authPath = join(getAgentDir(), "auth.json")) {
|
|
17
|
+
constructor(authPath = (process.env.PRISM_AUTH_PATH || process.env.PI_AUTH_PATH || join(getAgentDir(), "auth.json"))) {
|
|
18
18
|
this.authPath = normalizePath(authPath);
|
|
19
19
|
}
|
|
20
20
|
ensureParentDir() {
|
|
@@ -9,6 +9,8 @@ import { fileURLToPath } from "node:url";
|
|
|
9
9
|
import * as _bundledPiAgentCore from "@earendil-works/pi-agent-core";
|
|
10
10
|
import * as _bundledPiAiCompat from "@earendil-works/pi-ai/compat";
|
|
11
11
|
import * as _bundledPiAiOauth from "@earendil-works/pi-ai/oauth";
|
|
12
|
+
import * as _prismPiAiOpenAICompletions from "@earendil-works/pi-ai/api/openai-completions";
|
|
13
|
+
import * as _prismPiAiOpenAIResponses from "@earendil-works/pi-ai/api/openai-responses";
|
|
12
14
|
import * as _bundledPiAiProviders from "@earendil-works/pi-ai/providers/all";
|
|
13
15
|
import * as _bundledPiTui from "@earendil-works/pi-tui";
|
|
14
16
|
import { createJiti } from "jiti/static";
|
|
@@ -21,12 +23,14 @@ import * as _bundledTypeboxValue from "typebox/value";
|
|
|
21
23
|
import { CONFIG_DIR_NAME, getAgentDir, isBunBinary } from "../../config.js";
|
|
22
24
|
// NOTE: This import works because loader.ts exports are NOT re-exported from index.ts,
|
|
23
25
|
// avoiding a circular dependency. Extensions can import from @earendil-works/pi-coding-agent.
|
|
24
|
-
import * as _bundledPiCodingAgent from "
|
|
26
|
+
import * as _bundledPiCodingAgent from "./index.js";
|
|
25
27
|
import { resolvePath } from "../../utils/paths.js";
|
|
26
28
|
import { createEventBus } from "../event-bus.js";
|
|
27
29
|
import { execCommand } from "../exec.js";
|
|
28
30
|
import { readPiManifest } from "../pi-manifest.js";
|
|
29
31
|
import { createSyntheticSourceInfo } from "../source-info.js";
|
|
32
|
+
globalThis.__PRISM_PI_AI_OPENAI_COMPLETIONS__ = _prismPiAiOpenAICompletions;
|
|
33
|
+
globalThis.__PRISM_PI_AI_OPENAI_RESPONSES__ = _prismPiAiOpenAIResponses;
|
|
30
34
|
import { time } from "../timings.js";
|
|
31
35
|
/** Modules available to extensions via virtualModules (for compiled Bun binary) */
|
|
32
36
|
const VIRTUAL_MODULES = {
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { mkdirSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
const require = createRequire(import.meta.url);
|
|
6
|
+
export function getSessionBackend() {
|
|
7
|
+
return process.env.PRISM_SESSION_BACKEND || "db";
|
|
8
|
+
}
|
|
9
|
+
export function findMostRecentSessionFromDb(sessionDir) {
|
|
10
|
+
try {
|
|
11
|
+
const db = openDb();
|
|
12
|
+
const row = db.prepare("SELECT file_path FROM sessions WHERE session_dir = ? ORDER BY updated_at DESC LIMIT 1").get(sessionDir);
|
|
13
|
+
db.close();
|
|
14
|
+
return row?.file_path || null;
|
|
15
|
+
} catch { return null; }
|
|
16
|
+
}
|
|
17
|
+
export function deleteSessionFromDb(filePath) {
|
|
18
|
+
if (!filePath) return;
|
|
19
|
+
try {
|
|
20
|
+
const db = openDb();
|
|
21
|
+
db.prepare("DELETE FROM sessions WHERE file_path = ?").run(filePath);
|
|
22
|
+
db.close();
|
|
23
|
+
} catch { }
|
|
24
|
+
}
|
|
25
|
+
function openDb() {
|
|
26
|
+
const prismDir = process.env.PRISM_CODING_AGENT_DIR || process.env.PI_CODING_AGENT_DIR || join(homedir(), ".prism");
|
|
27
|
+
const dbPath = join(prismDir, "sessions.db");
|
|
28
|
+
mkdirSync(dirname(dbPath), { recursive: true });
|
|
29
|
+
const { DatabaseSync } = require("node:sqlite");
|
|
30
|
+
const db = new DatabaseSync(dbPath);
|
|
31
|
+
db.exec("PRAGMA journal_mode=WAL");
|
|
32
|
+
db.exec("PRAGMA synchronous=NORMAL");
|
|
33
|
+
db.exec("CREATE TABLE IF NOT EXISTS sessions (id TEXT PRIMARY KEY, file_path TEXT UNIQUE, cwd TEXT, session_dir TEXT, created_at TEXT, updated_at TEXT, parent_session TEXT, version INTEGER, header_json TEXT NOT NULL)");
|
|
34
|
+
db.exec("CREATE TABLE IF NOT EXISTS entries (session_id TEXT NOT NULL, entry_id TEXT NOT NULL, idx INTEGER NOT NULL, parent_id TEXT, type TEXT, timestamp TEXT, data_json TEXT NOT NULL, PRIMARY KEY (session_id, entry_id), FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE)");
|
|
35
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_entries_session_idx ON entries(session_id, idx)");
|
|
36
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_sessions_updated ON sessions(updated_at)");
|
|
37
|
+
return db;
|
|
38
|
+
}
|
|
39
|
+
export function mirrorSessionFileToDb(filePath, entries, cwd, sessionDir) {
|
|
40
|
+
if (!filePath) return;
|
|
41
|
+
const header = entries.find((entry) => entry?.type === "session" && typeof entry.id === "string");
|
|
42
|
+
if (!header) return;
|
|
43
|
+
const rows = entries.filter((entry) => entry !== header);
|
|
44
|
+
const db = openDb();
|
|
45
|
+
const updatedAt = rows.at(-1)?.timestamp || header.timestamp || new Date().toISOString();
|
|
46
|
+
const insertSession = db.prepare("INSERT INTO sessions (id, file_path, cwd, session_dir, created_at, updated_at, parent_session, version, header_json) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET file_path=excluded.file_path, cwd=excluded.cwd, session_dir=excluded.session_dir, updated_at=excluded.updated_at, parent_session=excluded.parent_session, version=excluded.version, header_json=excluded.header_json");
|
|
47
|
+
const insertEntry = db.prepare("INSERT INTO entries (session_id, entry_id, idx, parent_id, type, timestamp, data_json) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT(session_id, entry_id) DO UPDATE SET idx=excluded.idx, parent_id=excluded.parent_id, type=excluded.type, timestamp=excluded.timestamp, data_json=excluded.data_json");
|
|
48
|
+
const deleteEntries = db.prepare("DELETE FROM entries WHERE session_id = ?");
|
|
49
|
+
db.exec("BEGIN");
|
|
50
|
+
try {
|
|
51
|
+
insertSession.run(header.id, filePath, header.cwd || cwd || "", sessionDir || dirname(filePath), header.timestamp || "", updatedAt, header.parentSession || null, header.version || null, JSON.stringify(header));
|
|
52
|
+
deleteEntries.run(header.id);
|
|
53
|
+
rows.forEach((entry, idx) => insertEntry.run(header.id, typeof entry.id === "string" ? entry.id : String(idx), idx, entry.parentId ?? null, entry.type || null, entry.timestamp || null, JSON.stringify(entry)));
|
|
54
|
+
db.exec("COMMIT");
|
|
55
|
+
} catch (error) {
|
|
56
|
+
db.exec("ROLLBACK");
|
|
57
|
+
db.close();
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
db.close();
|
|
61
|
+
}
|
|
62
|
+
export function mirrorEntryToDb(filePath, header, entry, cwd, sessionDir, index) {
|
|
63
|
+
if (!filePath || !header?.id || !entry) return;
|
|
64
|
+
const db = openDb();
|
|
65
|
+
const insertSession = db.prepare("INSERT INTO sessions (id, file_path, cwd, session_dir, created_at, updated_at, parent_session, version, header_json) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET file_path=excluded.file_path, cwd=excluded.cwd, session_dir=excluded.session_dir, updated_at=excluded.updated_at, parent_session=excluded.parent_session, version=excluded.version, header_json=excluded.header_json");
|
|
66
|
+
const insertEntry = db.prepare("INSERT INTO entries (session_id, entry_id, idx, parent_id, type, timestamp, data_json) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT(session_id, entry_id) DO UPDATE SET idx=excluded.idx, parent_id=excluded.parent_id, type=excluded.type, timestamp=excluded.timestamp, data_json=excluded.data_json");
|
|
67
|
+
insertSession.run(header.id, filePath, header.cwd || cwd || "", sessionDir || dirname(filePath), header.timestamp || "", entry.timestamp || header.timestamp || new Date().toISOString(), header.parentSession || null, header.version || null, JSON.stringify(header));
|
|
68
|
+
insertEntry.run(header.id, typeof entry.id === "string" ? entry.id : String(index ?? 0), Number.isInteger(index) ? index : 0, entry.parentId ?? null, entry.type || null, entry.timestamp || null, JSON.stringify(entry));
|
|
69
|
+
db.close();
|
|
70
|
+
}
|
|
71
|
+
export function loadEntriesFromDb(filePath) {
|
|
72
|
+
if (!filePath) return null;
|
|
73
|
+
try {
|
|
74
|
+
const db = openDb();
|
|
75
|
+
const session = db.prepare("SELECT id, header_json FROM sessions WHERE file_path = ?").get(filePath);
|
|
76
|
+
if (!session) { db.close(); return null; }
|
|
77
|
+
const entryRows = db.prepare("SELECT data_json FROM entries WHERE session_id = ? ORDER BY idx ASC").all(session.id);
|
|
78
|
+
db.close();
|
|
79
|
+
return [JSON.parse(session.header_json), ...entryRows.map((r) => JSON.parse(r.data_json))];
|
|
80
|
+
} catch { return null; }
|
|
81
|
+
}
|
|
82
|
+
async function buildSessionsFromRows(db, sessionRows, onProgress) {
|
|
83
|
+
const sessions = [];
|
|
84
|
+
let loaded = 0;
|
|
85
|
+
const total = sessionRows.length;
|
|
86
|
+
for (const row of sessionRows) {
|
|
87
|
+
try {
|
|
88
|
+
const entryRows = db.prepare("SELECT data_json FROM entries WHERE session_id = ? ORDER BY idx ASC").all(row.id);
|
|
89
|
+
let messageCount = 0, firstMessage = "";
|
|
90
|
+
const allMessages = [];
|
|
91
|
+
let name;
|
|
92
|
+
for (const { data_json } of entryRows) {
|
|
93
|
+
const e = JSON.parse(data_json);
|
|
94
|
+
if (e.type === "session_info" && e.name?.trim()) name = e.name.trim();
|
|
95
|
+
if (e.type !== "message") continue;
|
|
96
|
+
messageCount++;
|
|
97
|
+
const msg = e.message;
|
|
98
|
+
if (!msg?.content || (msg.role !== "user" && msg.role !== "assistant")) continue;
|
|
99
|
+
const text = [].concat(msg.content).filter((p) => p?.type === "text").map((p) => p.text).join(" ").trim();
|
|
100
|
+
if (!text) continue;
|
|
101
|
+
allMessages.push(text);
|
|
102
|
+
if (!firstMessage && msg.role === "user") firstMessage = text;
|
|
103
|
+
}
|
|
104
|
+
sessions.push({ path: row.file_path || row.id, id: row.id, cwd: row.cwd || "", name, parentSessionPath: row.parent_session || undefined, created: new Date(row.created_at || 0), modified: new Date(row.updated_at || row.created_at || 0), messageCount, firstMessage: firstMessage || "(no messages)", allMessagesText: allMessages.join(" ") });
|
|
105
|
+
} catch {}
|
|
106
|
+
loaded++;
|
|
107
|
+
onProgress?.(loaded, total);
|
|
108
|
+
}
|
|
109
|
+
return sessions;
|
|
110
|
+
}
|
|
111
|
+
export async function listSessionsFromDb(dir, onProgress) {
|
|
112
|
+
try {
|
|
113
|
+
const db = openDb();
|
|
114
|
+
const sessionRows = db.prepare("SELECT id, file_path, cwd, created_at, updated_at, parent_session FROM sessions WHERE session_dir = ? ORDER BY updated_at DESC").all(dir);
|
|
115
|
+
const sessions = await buildSessionsFromRows(db, sessionRows, onProgress);
|
|
116
|
+
db.close();
|
|
117
|
+
return sessions;
|
|
118
|
+
} catch { return []; }
|
|
119
|
+
}
|
|
120
|
+
export async function listAllSessionsFromDb(onProgress) {
|
|
121
|
+
try {
|
|
122
|
+
const db = openDb();
|
|
123
|
+
const sessionRows = db.prepare("SELECT id, file_path, cwd, created_at, updated_at, parent_session FROM sessions ORDER BY updated_at DESC").all();
|
|
124
|
+
const sessions = await buildSessionsFromRows(db, sessionRows, onProgress);
|
|
125
|
+
db.close();
|
|
126
|
+
return sessions;
|
|
127
|
+
} catch { return []; }
|
|
128
|
+
}
|
|
@@ -2,7 +2,7 @@ import { APP_NAME, CONFIG_DIR_NAME } from "../config.js";
|
|
|
2
2
|
import { emitProjectTrustEvent } from "./extensions/runner.js";
|
|
3
3
|
import { getProjectTrustOptions, hasTrustRequiringProjectResources, } from "./trust-manager.js";
|
|
4
4
|
function formatProjectTrustPrompt(cwd) {
|
|
5
|
-
return `Trust project folder?\n${cwd}\n\nThis allows
|
|
5
|
+
return `Trust project folder?\n${cwd}\n\nThis allows Prism to load ${CONFIG_DIR_NAME} settings and resources, install missing project packages, and execute project extensions.`;
|
|
6
6
|
}
|
|
7
7
|
async function selectProjectTrustOption(cwd, ctx) {
|
|
8
8
|
const options = getProjectTrustOptions(cwd, { includeSessionOnly: true });
|
|
@@ -67,7 +67,7 @@ export async function createAgentSession(options = {}) {
|
|
|
67
67
|
const cwd = resolvePath(options.cwd ?? options.sessionManager?.getCwd() ?? process.cwd());
|
|
68
68
|
const agentDir = options.agentDir ? resolvePath(options.agentDir) : getDefaultAgentDir();
|
|
69
69
|
let resourceLoader = options.resourceLoader;
|
|
70
|
-
const authPath = options.agentDir ? join(agentDir, "auth.json") : undefined;
|
|
70
|
+
const authPath = process.env.PRISM_AUTH_PATH || process.env.PI_AUTH_PATH || (options.agentDir ? join(agentDir, "auth.json") : undefined);
|
|
71
71
|
const modelsPath = options.agentDir ? join(agentDir, "models.json") : undefined;
|
|
72
72
|
const modelRuntime = options.modelRuntime ?? (await ModelRuntime.create({ authPath, modelsPath }));
|
|
73
73
|
const settingsManager = options.settingsManager ?? SettingsManager.create(cwd, agentDir);
|
|
@@ -3,6 +3,7 @@ import { randomUUID } from "crypto";
|
|
|
3
3
|
import { appendFileSync, closeSync, createReadStream, existsSync, mkdirSync, openSync, readdirSync, readSync, statSync, writeFileSync, } from "fs";
|
|
4
4
|
import { readdir, stat } from "fs/promises";
|
|
5
5
|
import { join, resolve } from "path";
|
|
6
|
+
import { findMostRecentSessionFromDb, getSessionBackend, listAllSessionsFromDb, listSessionsFromDb, loadEntriesFromDb, mirrorEntryToDb, mirrorSessionFileToDb } from "./prism-session-db.js";
|
|
6
7
|
import { createInterface } from "readline";
|
|
7
8
|
import { StringDecoder } from "string_decoder";
|
|
8
9
|
import { APP_NAME, getAgentDir as getDefaultAgentDir, getSessionsDir } from "../config.js";
|
|
@@ -634,12 +635,23 @@ export class SessionManager {
|
|
|
634
635
|
this._rewriteFile();
|
|
635
636
|
}
|
|
636
637
|
this._buildIndex();
|
|
638
|
+
if (getSessionBackend() !== "file") { try { mirrorSessionFileToDb(this.sessionFile, this.fileEntries, this.cwd, this.sessionDir); } catch { } }
|
|
637
639
|
this.flushed = true;
|
|
638
640
|
}
|
|
639
641
|
else {
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
|
|
642
|
+
const dbEntries = getSessionBackend() !== "file" ? loadEntriesFromDb(this.sessionFile) : null;
|
|
643
|
+
if (dbEntries) {
|
|
644
|
+
this.fileEntries = dbEntries;
|
|
645
|
+
const header = this.fileEntries.find((e) => e.type === "session");
|
|
646
|
+
this.sessionId = header?.id ?? createSessionId();
|
|
647
|
+
this._buildIndex();
|
|
648
|
+
if (getSessionBackend() !== "file") { try { mirrorSessionFileToDb(this.sessionFile, this.fileEntries, this.cwd, this.sessionDir); } catch {} }
|
|
649
|
+
this.flushed = true;
|
|
650
|
+
} else {
|
|
651
|
+
const explicitPath = this.sessionFile;
|
|
652
|
+
this.newSession();
|
|
653
|
+
this.sessionFile = explicitPath;
|
|
654
|
+
}
|
|
643
655
|
}
|
|
644
656
|
}
|
|
645
657
|
newSession(options) {
|
|
@@ -693,6 +705,8 @@ export class SessionManager {
|
|
|
693
705
|
_rewriteFile() {
|
|
694
706
|
if (!this.persist || !this.sessionFile)
|
|
695
707
|
return;
|
|
708
|
+
if (getSessionBackend() !== "file") { try { mirrorSessionFileToDb(this.sessionFile, this.fileEntries, this.cwd, this.sessionDir); } catch { } }
|
|
709
|
+
if (getSessionBackend() === "db") return;
|
|
696
710
|
const fd = openSync(this.sessionFile, "w");
|
|
697
711
|
try {
|
|
698
712
|
for (const entry of this.fileEntries) {
|
|
@@ -724,30 +738,14 @@ export class SessionManager {
|
|
|
724
738
|
_persist(entry) {
|
|
725
739
|
if (!this.persist || !this.sessionFile)
|
|
726
740
|
return;
|
|
727
|
-
|
|
728
|
-
if
|
|
729
|
-
|
|
730
|
-
appendFileSync(this.sessionFile, `${JSON.stringify(entry)}\n`);
|
|
731
|
-
}
|
|
732
|
-
else {
|
|
733
|
-
// Mark as not flushed so when assistant arrives, all entries get written
|
|
734
|
-
this.flushed = false;
|
|
735
|
-
}
|
|
736
|
-
return;
|
|
737
|
-
}
|
|
741
|
+
// Prism: persist every entry immediately. Pi defers session writes until the
|
|
742
|
+
// first assistant message, which can lose user prompts if multiple harnesses
|
|
743
|
+
// run at once or a process exits before the assistant response is recorded.
|
|
738
744
|
if (!this.flushed) {
|
|
739
|
-
const fd = openSync(this.sessionFile, "wx");
|
|
740
|
-
try {
|
|
741
|
-
for (const e of this.fileEntries) {
|
|
742
|
-
writeFileSync(fd, `${JSON.stringify(e)}\n`);
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
finally {
|
|
746
|
-
closeSync(fd);
|
|
747
|
-
}
|
|
745
|
+
if (getSessionBackend() !== "db") { const fd = openSync(this.sessionFile, "wx"); try { for (const e of this.fileEntries) { writeFileSync(fd, `${JSON.stringify(e)}\n`); } } finally { closeSync(fd); } }
|
|
748
746
|
this.flushed = true;
|
|
749
747
|
}
|
|
750
|
-
else {
|
|
748
|
+
else if (getSessionBackend() !== "db") {
|
|
751
749
|
appendFileSync(this.sessionFile, `${JSON.stringify(entry)}\n`);
|
|
752
750
|
}
|
|
753
751
|
}
|
|
@@ -756,6 +754,7 @@ export class SessionManager {
|
|
|
756
754
|
this.byId.set(entry.id, entry);
|
|
757
755
|
this.leafId = entry.id;
|
|
758
756
|
this._persist(entry);
|
|
757
|
+
if (getSessionBackend() !== "file") { try { mirrorEntryToDb(this.sessionFile, this.fileEntries.find((e) => e.type === "session"), entry, this.cwd, this.sessionDir, this.fileEntries.length - 2); } catch { } }
|
|
759
758
|
}
|
|
760
759
|
/** Append a message as child of current leaf, then advance leaf. Returns entry id.
|
|
761
760
|
* Does not allow writing CompactionSummaryMessage and BranchSummaryMessage directly.
|
|
@@ -1201,6 +1200,7 @@ export class SessionManager {
|
|
|
1201
1200
|
header = firstEntry?.type === "session" ? firstEntry : null;
|
|
1202
1201
|
}
|
|
1203
1202
|
}
|
|
1203
|
+
if (!header && getSessionBackend() !== "file") { try { preloadedFileEntries = loadEntriesFromDb(resolvedPath) ?? undefined; header = preloadedFileEntries?.find((e) => e.type === "session") ?? null; } catch {} }
|
|
1204
1204
|
const cwd = cwdOverride ?? (header ? getSessionHeaderCwd(header) : undefined) ?? process.cwd();
|
|
1205
1205
|
// If no sessionDir provided, derive from file's parent directory
|
|
1206
1206
|
const dir = sessionDir ? normalizePath(sessionDir) : resolve(resolvedPath, "..");
|
|
@@ -1214,7 +1214,7 @@ export class SessionManager {
|
|
|
1214
1214
|
static continueRecent(cwd, sessionDir) {
|
|
1215
1215
|
const dir = sessionDir ? normalizePath(sessionDir) : getDefaultSessionDir(cwd);
|
|
1216
1216
|
const filterCwd = sessionDir !== undefined && dir !== getDefaultSessionDirPath(cwd);
|
|
1217
|
-
const mostRecent = findMostRecentSession(dir, filterCwd ? cwd : undefined);
|
|
1217
|
+
const mostRecent = getSessionBackend() === "file" ? findMostRecentSession(dir, filterCwd ? cwd : undefined) : findMostRecentSessionFromDb(dir);
|
|
1218
1218
|
if (mostRecent) {
|
|
1219
1219
|
return new SessionManager(cwd, dir, mostRecent, true);
|
|
1220
1220
|
}
|
|
@@ -1234,7 +1234,8 @@ export class SessionManager {
|
|
|
1234
1234
|
static forkFrom(sourcePath, targetCwd, sessionDir, options) {
|
|
1235
1235
|
const resolvedSourcePath = resolvePath(sourcePath);
|
|
1236
1236
|
const resolvedTargetCwd = resolvePath(targetCwd);
|
|
1237
|
-
|
|
1237
|
+
let sourceEntries = loadEntriesFromFile(resolvedSourcePath);
|
|
1238
|
+
if (sourceEntries.length === 0 && getSessionBackend() !== "file") { try { sourceEntries = loadEntriesFromDb(resolvedSourcePath) ?? []; } catch {} }
|
|
1238
1239
|
if (sourceEntries.length === 0) {
|
|
1239
1240
|
throw new Error(`Cannot fork: source session file is empty or invalid: ${resolvedSourcePath}`);
|
|
1240
1241
|
}
|
|
@@ -1263,12 +1264,10 @@ export class SessionManager {
|
|
|
1263
1264
|
cwd: resolvedTargetCwd,
|
|
1264
1265
|
parentSession: resolvedSourcePath,
|
|
1265
1266
|
};
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
if (entry.type !== "session") {
|
|
1270
|
-
appendFileSync(newSessionFile, `${JSON.stringify(entry)}\n`);
|
|
1271
|
-
}
|
|
1267
|
+
if (getSessionBackend() !== "file") { try { mirrorSessionFileToDb(newSessionFile, [newHeader, ...sourceEntries.filter((e) => e.type !== "session")], resolvedTargetCwd, dir); } catch {} }
|
|
1268
|
+
if (getSessionBackend() !== "db") {
|
|
1269
|
+
writeFileSync(newSessionFile, `${JSON.stringify(newHeader)}\n`, { flag: "wx" });
|
|
1270
|
+
for (const entry of sourceEntries) { if (entry.type !== "session") appendFileSync(newSessionFile, `${JSON.stringify(entry)}\n`); }
|
|
1272
1271
|
}
|
|
1273
1272
|
return new SessionManager(resolvedTargetCwd, dir, newSessionFile, true);
|
|
1274
1273
|
}
|
|
@@ -1280,11 +1279,14 @@ export class SessionManager {
|
|
|
1280
1279
|
*/
|
|
1281
1280
|
static async list(cwd, sessionDir, onProgress) {
|
|
1282
1281
|
const dir = sessionDir ? normalizePath(sessionDir) : getDefaultSessionDir(cwd);
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1282
|
+
if (getSessionBackend() === "file") {
|
|
1283
|
+
const filterCwd = sessionDir !== undefined && dir !== getDefaultSessionDirPath(cwd);
|
|
1284
|
+
const resolvedCwd = resolvePath(cwd);
|
|
1285
|
+
const sessions = (await listSessionsFromDir(dir, onProgress)).filter((session) => !filterCwd || sessionCwdMatches(session.cwd, resolvedCwd));
|
|
1286
|
+
sessions.sort((a, b) => b.modified.getTime() - a.modified.getTime());
|
|
1287
|
+
return sessions;
|
|
1288
|
+
}
|
|
1289
|
+
return listSessionsFromDb(dir, onProgress);
|
|
1288
1290
|
}
|
|
1289
1291
|
static async listAll(sessionDirOrOnProgress, onProgress) {
|
|
1290
1292
|
const customSessionDir = typeof sessionDirOrOnProgress === "string" ? normalizePath(sessionDirOrOnProgress) : undefined;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* System prompt construction and project context loading
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
5
6
|
import { formatSkillsForPrompt } from "./skills.js";
|
|
6
7
|
/** Build the system prompt with tools, guidelines, and context */
|
|
7
8
|
export function buildSystemPrompt(options) {
|
|
@@ -32,10 +33,22 @@ export function buildSystemPrompt(options) {
|
|
|
32
33
|
prompt += `\nCurrent working directory: ${promptCwd}\n`;
|
|
33
34
|
return prompt;
|
|
34
35
|
}
|
|
35
|
-
// Get absolute paths to documentation and examples
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
36
|
+
// Get absolute paths to project documentation and examples.
|
|
37
|
+
// Only project docs are advertised here. Harness/pi docs can be linked into the project docs
|
|
38
|
+
// tree (for example docs/pi) when a project wants them in the prompt.
|
|
39
|
+
const resolvedCwd = cwd;
|
|
40
|
+
const existingPath = (filePath) => existsSync(filePath) ? filePath : undefined;
|
|
41
|
+
const firstExistingPath = (filePaths) => filePaths.find((filePath) => existsSync(filePath));
|
|
42
|
+
const projectReadmePath = firstExistingPath(["README.md", "README.MD", "readme.md"].map((name) => join(resolvedCwd, name)));
|
|
43
|
+
const projectDocsPath = existingPath(join(resolvedCwd, "docs"));
|
|
44
|
+
const projectExamplesPath = existingPath(join(resolvedCwd, "examples"));
|
|
45
|
+
const documentationLines = [];
|
|
46
|
+
if (projectReadmePath) documentationLines.push(`- Main documentation: ${projectReadmePath}`);
|
|
47
|
+
if (projectDocsPath) documentationLines.push(`- Additional docs: ${projectDocsPath}`);
|
|
48
|
+
if (projectExamplesPath) documentationLines.push(`- Examples: ${projectExamplesPath}`);
|
|
49
|
+
if (projectDocsPath || projectExamplesPath) documentationLines.push("- When reading these docs or examples, resolve docs/... under Additional docs and examples/... under Examples, not the current working directory");
|
|
50
|
+
if (projectDocsPath) documentationLines.push("- When working on this project, read the relevant docs and follow .md cross-references before implementing");
|
|
51
|
+
const documentationSection = documentationLines.length > 0 ? `\n\nProject documentation (read only when relevant):\n${documentationLines.join("\n")}` : "";
|
|
39
52
|
// Build tools list based on selected tools.
|
|
40
53
|
// A tool appears in Available tools only when the caller provides a one-line snippet.
|
|
41
54
|
const tools = selectedTools || ["read", "bash", "edit", "write"];
|
|
@@ -70,7 +83,7 @@ export function buildSystemPrompt(options) {
|
|
|
70
83
|
addGuideline("Be concise in your responses");
|
|
71
84
|
addGuideline("Show file paths clearly when working with files");
|
|
72
85
|
const guidelines = guidelinesList.map((g) => `- ${g}`).join("\n");
|
|
73
|
-
let prompt = `You are an expert coding assistant operating inside
|
|
86
|
+
let prompt = `You are an expert coding assistant operating inside prism, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.
|
|
74
87
|
|
|
75
88
|
Available tools:
|
|
76
89
|
${toolsList}
|
|
@@ -78,16 +91,7 @@ ${toolsList}
|
|
|
78
91
|
In addition to the tools above, you may have access to other custom tools depending on the project.
|
|
79
92
|
|
|
80
93
|
Guidelines:
|
|
81
|
-
${guidelines}
|
|
82
|
-
|
|
83
|
-
Pi documentation (read only when the user asks about pi itself, its SDK, extensions, themes, skills, or TUI):
|
|
84
|
-
- Main documentation: ${readmePath}
|
|
85
|
-
- Additional docs: ${docsPath}
|
|
86
|
-
- Examples: ${examplesPath} (extensions, custom tools, SDK)
|
|
87
|
-
- When reading pi docs or examples, resolve docs/... under Additional docs and examples/... under Examples, not the current working directory
|
|
88
|
-
- When asked about: extensions (docs/extensions.md, examples/extensions/), themes (docs/themes.md), skills (docs/skills.md), prompt templates (docs/prompt-templates.md), TUI components (docs/tui.md), keybindings (docs/keybindings.md), SDK integrations (docs/sdk.md), custom providers (docs/custom-provider.md), adding models (docs/models.md), pi packages (docs/packages.md), environment variables (docs/environment-variables.md)
|
|
89
|
-
- When working on pi topics, read the docs and examples, and follow .md cross-references before implementing
|
|
90
|
-
- Always read pi .md files completely and follow links to related docs (e.g., tui.md for TUI API details)`;
|
|
94
|
+
${guidelines}${documentationSection}`;
|
|
91
95
|
if (appendSection) {
|
|
92
96
|
prompt += appendSection;
|
|
93
97
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { deleteSessionFromDb } from "../../../core/prism-session-db.js";
|
|
2
3
|
import { existsSync } from "node:fs";
|
|
3
4
|
import { unlink } from "node:fs/promises";
|
|
4
5
|
import * as os from "node:os";
|
|
@@ -539,6 +540,9 @@ class SessionList {
|
|
|
539
540
|
* Delete a session file, trying the `trash` CLI first, then falling back to unlink
|
|
540
541
|
*/
|
|
541
542
|
async function deleteSessionFile(sessionPath) {
|
|
543
|
+
const _backend = process.env.PRISM_SESSION_BACKEND || "db";
|
|
544
|
+
if (_backend !== "file") { try { deleteSessionFromDb(sessionPath); } catch {} }
|
|
545
|
+
if (_backend === "db" && !existsSync(sessionPath)) return { ok: true, method: "trash" };
|
|
542
546
|
// Try `trash` first (if installed)
|
|
543
547
|
const trashArgs = sessionPath.startsWith("-") ? ["--", sessionPath] : [sessionPath];
|
|
544
548
|
const trashResult = spawnSync("trash", trashArgs, { encoding: "utf-8" });
|
package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js
CHANGED
|
@@ -484,12 +484,15 @@ export class InteractiveMode {
|
|
|
484
484
|
}));
|
|
485
485
|
// Convert extension commands to SlashCommand format
|
|
486
486
|
const builtinCommandNames = new Set(slashCommands.map((c) => c.name));
|
|
487
|
+
const prismInternalCommandExtensions = new Set(["caveman-mode.ts", "clear-session.ts", "eval-command.ts", "git-commit.ts", "plan-command.ts", "prompt-review-command.ts", "self-improve-command.ts", "voice-command.ts", "vote-command.ts", "obsidian-memory.ts", "logseq-memory.ts", "honcho-memory.ts", "research-tools.ts", "ai-memory-system.ts", "rp-memory.ts", "manifest-provider.ts", "ollama-provider.ts", "toolbox.ts", "self-improvement-tools.ts", "context-usage.ts", "prism-status.ts", "toolbar.ts", "pi-mcp-adapter"]);
|
|
488
|
+
const prismInternalCommandNpmPackages = new Set(["pi-mcp-adapter"]);
|
|
489
|
+
const isPrismInternalCommand = (cmd) => (() => { const prismExtensionPath = String(cmd.sourceInfo?.path || "").replaceAll("\\", "/"); const prismExtensionSource = String(cmd.sourceInfo?.source || ""); return prismInternalCommandExtensions.has(path.basename(prismExtensionPath)) || Array.from(prismInternalCommandNpmPackages).some((pkg) => prismExtensionSource === "npm:" + pkg || prismExtensionPath.includes("/node_modules/" + pkg + "/")); })();
|
|
487
490
|
const extensionCommands = this.session.extensionRunner
|
|
488
491
|
.getRegisteredCommands()
|
|
489
492
|
.filter((cmd) => !builtinCommandNames.has(cmd.name))
|
|
490
493
|
.map((cmd) => ({
|
|
491
494
|
name: cmd.invocationName,
|
|
492
|
-
description: this.prefixAutocompleteDescription(cmd.description, cmd.sourceInfo),
|
|
495
|
+
description: isPrismInternalCommand(cmd) ? cmd.description : this.prefixAutocompleteDescription(cmd.description, cmd.sourceInfo),
|
|
493
496
|
getArgumentCompletions: cmd.getArgumentCompletions,
|
|
494
497
|
}));
|
|
495
498
|
// Build skill commands from session.skills (if enabled)
|
|
@@ -676,7 +679,7 @@ export class InteractiveMode {
|
|
|
676
679
|
await this.themeController.applyFromSettings();
|
|
677
680
|
// Add header with keybindings from config (unless silenced)
|
|
678
681
|
if (this.options.verbose || !this.settingsManager.getQuietStartup()) {
|
|
679
|
-
const logo =
|
|
682
|
+
const logo = "▲ \u001b[38;5;196mp\u001b[38;5;202mr\u001b[38;5;226mi\u001b[38;5;46ms\u001b[38;5;51mm\u001b[0m v0.2.92";
|
|
680
683
|
// Build startup instructions using keybinding hint helpers
|
|
681
684
|
const hint = (keybinding, description) => keyHint(keybinding, description);
|
|
682
685
|
const expandedInstructions = [
|
|
@@ -708,7 +711,7 @@ export class InteractiveMode {
|
|
|
708
711
|
hint("app.tools.expand", "more"),
|
|
709
712
|
].join(theme.fg("muted", " · "));
|
|
710
713
|
const compactOnboarding = theme.fg("dim", `Press ${keyText("app.tools.expand")} to show full startup help and loaded resources.`);
|
|
711
|
-
const onboarding = theme.fg("dim", `
|
|
714
|
+
const onboarding = theme.fg("dim", `Prism can explain its own features and look up its docs. Ask it how to use or extend Prism.`);
|
|
712
715
|
this.builtInHeader = new ExpandableText(() => `${logo}\n${compactInstructions}\n${compactOnboarding}\n\n${onboarding}`, () => `${logo}\n${expandedInstructions}\n\n${onboarding}`, this.getStartupExpansionState(), 1, 0);
|
|
713
716
|
// Setup UI layout
|
|
714
717
|
this.headerContainer.addChild(new Spacer(1));
|
|
@@ -757,10 +760,10 @@ export class InteractiveMode {
|
|
|
757
760
|
const cwdBasename = path.basename(this.sessionManager.getCwd());
|
|
758
761
|
const sessionName = this.sessionManager.getSessionName();
|
|
759
762
|
if (sessionName) {
|
|
760
|
-
this.ui.terminal.setTitle(
|
|
763
|
+
this.ui.terminal.setTitle(`▲ - ${sessionName} - ${cwdBasename}`);
|
|
761
764
|
}
|
|
762
765
|
else {
|
|
763
|
-
this.ui.terminal.setTitle(
|
|
766
|
+
this.ui.terminal.setTitle(`▲ - ${cwdBasename}`);
|
|
764
767
|
}
|
|
765
768
|
}
|
|
766
769
|
/**
|
|
@@ -823,6 +826,7 @@ export class InteractiveMode {
|
|
|
823
826
|
}
|
|
824
827
|
catch (error) {
|
|
825
828
|
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
829
|
+
errorMessage = this.humanizeUsageLimitError(errorMessage);
|
|
826
830
|
this.showError(errorMessage);
|
|
827
831
|
}
|
|
828
832
|
}
|
|
@@ -833,7 +837,8 @@ export class InteractiveMode {
|
|
|
833
837
|
}
|
|
834
838
|
catch (error) {
|
|
835
839
|
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
836
|
-
this.
|
|
840
|
+
errorMessage = this.humanizeUsageLimitError(errorMessage);
|
|
841
|
+
this.showError(errorMessage);
|
|
837
842
|
}
|
|
838
843
|
}
|
|
839
844
|
}
|
|
@@ -845,6 +850,7 @@ export class InteractiveMode {
|
|
|
845
850
|
}
|
|
846
851
|
catch (error) {
|
|
847
852
|
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
853
|
+
errorMessage = this.humanizeUsageLimitError(errorMessage);
|
|
848
854
|
this.showError(errorMessage);
|
|
849
855
|
}
|
|
850
856
|
}
|
|
@@ -921,14 +927,19 @@ export class InteractiveMode {
|
|
|
921
927
|
const entries = parseChangelog(changelogPath);
|
|
922
928
|
if (!lastVersion) {
|
|
923
929
|
// Fresh install - record the version, send telemetry, don't show changelog
|
|
924
|
-
this.settingsManager.setLastChangelogVersion(
|
|
925
|
-
this.reportInstallTelemetry(
|
|
930
|
+
this.settingsManager.setLastChangelogVersion("0.84.2");
|
|
931
|
+
this.reportInstallTelemetry("0.84.2");
|
|
926
932
|
return undefined;
|
|
927
933
|
}
|
|
928
934
|
const newEntries = getNewEntries(entries, lastVersion);
|
|
935
|
+
if (entries.length > 0 && newEntries.length === entries.length) {
|
|
936
|
+
this.settingsManager.setLastChangelogVersion("0.84.2");
|
|
937
|
+
this.reportInstallTelemetry("0.84.2");
|
|
938
|
+
return normalizeChangelogLinks(entries[0].content, entries[0]);
|
|
939
|
+
}
|
|
929
940
|
if (newEntries.length > 0) {
|
|
930
|
-
this.settingsManager.setLastChangelogVersion(
|
|
931
|
-
this.reportInstallTelemetry(
|
|
941
|
+
this.settingsManager.setLastChangelogVersion("0.84.2");
|
|
942
|
+
this.reportInstallTelemetry("0.84.2");
|
|
932
943
|
return newEntries.map((e) => normalizeChangelogLinks(e.content, e)).join("\n\n");
|
|
933
944
|
}
|
|
934
945
|
return undefined;
|
|
@@ -1266,7 +1277,7 @@ export class InteractiveMode {
|
|
|
1266
1277
|
const skillsResult = this.session.resourceLoader.getSkills();
|
|
1267
1278
|
const promptsResult = this.session.resourceLoader.getPrompts();
|
|
1268
1279
|
const themesResult = this.session.resourceLoader.getThemes();
|
|
1269
|
-
|
|
1280
|
+
let extensions = options?.extensions ??
|
|
1270
1281
|
this.session.resourceLoader
|
|
1271
1282
|
.getExtensions()
|
|
1272
1283
|
.extensions.filter((extension) => !extension.hidden)
|
|
@@ -1337,6 +1348,9 @@ export class InteractiveMode {
|
|
|
1337
1348
|
const promptCompactList = formatCompactList(templates.map((template) => `/${template.name}`));
|
|
1338
1349
|
addLoadedSection("Prompts", promptCompactList, templateList);
|
|
1339
1350
|
}
|
|
1351
|
+
const prismInternalExtensions = new Set(["caveman-mode.ts", "clear-session.ts", "eval-command.ts", "git-commit.ts", "plan-command.ts", "prompt-review-command.ts", "self-improve-command.ts", "voice-command.ts", "vote-command.ts", "obsidian-memory.ts", "logseq-memory.ts", "honcho-memory.ts", "research-tools.ts", "ai-memory-system.ts", "rp-memory.ts", "manifest-provider.ts", "ollama-provider.ts", "toolbox.ts", "self-improvement-tools.ts", "context-usage.ts", "prism-status.ts", "toolbar.ts", "pi-mcp-adapter"]);
|
|
1352
|
+
const prismInternalNpmPackages = new Set(["pi-mcp-adapter"]);
|
|
1353
|
+
extensions = extensions.filter((extension) => !(() => { const prismExtensionPath = String(extension.path || "").replaceAll("\\", "/"); const prismExtensionSource = String(extension.sourceInfo?.source || ""); return prismInternalExtensions.has(path.basename(prismExtensionPath)) || Array.from(prismInternalNpmPackages).some((pkg) => prismExtensionSource === "npm:" + pkg || prismExtensionPath.includes("/node_modules/" + pkg + "/")); })());
|
|
1340
1354
|
if (extensions.length > 0) {
|
|
1341
1355
|
const groups = this.buildScopeGroups(extensions);
|
|
1342
1356
|
const extList = this.formatScopeGroups(groups, {
|
|
@@ -1348,7 +1362,8 @@ export class InteractiveMode {
|
|
|
1348
1362
|
}
|
|
1349
1363
|
// Show loaded themes (excluding built-in)
|
|
1350
1364
|
const loadedThemes = themesResult.themes;
|
|
1351
|
-
const
|
|
1365
|
+
const prismInternalThemes = new Set(["soft-green"]);
|
|
1366
|
+
const customThemes = loadedThemes.filter((t) => t.sourcePath && !prismInternalThemes.has(t.name));
|
|
1352
1367
|
if (customThemes.length > 0) {
|
|
1353
1368
|
const groups = this.buildScopeGroups(customThemes.map((loadedTheme) => ({
|
|
1354
1369
|
path: loadedTheme.sourcePath,
|
|
@@ -2428,6 +2443,27 @@ export class InteractiveMode {
|
|
|
2428
2443
|
await this.handleCompactCommand(customInstructions);
|
|
2429
2444
|
return;
|
|
2430
2445
|
}
|
|
2446
|
+
if (text === "/plan" || text.startsWith("/plan ")) {
|
|
2447
|
+
const task = text.startsWith("/plan ") ? text.slice(6).trim() : "";
|
|
2448
|
+
this.editor.setText("");
|
|
2449
|
+
await this.startPrismPlanMode(task);
|
|
2450
|
+
return;
|
|
2451
|
+
}
|
|
2452
|
+
if (text === "/plan:approve") {
|
|
2453
|
+
this.editor.setText("");
|
|
2454
|
+
await this.approvePrismPlan();
|
|
2455
|
+
return;
|
|
2456
|
+
}
|
|
2457
|
+
if (text === "/plan:reject") {
|
|
2458
|
+
this.editor.setText("");
|
|
2459
|
+
this.rejectPrismPlan();
|
|
2460
|
+
return;
|
|
2461
|
+
}
|
|
2462
|
+
if (text === "/plan:cancel") {
|
|
2463
|
+
this.editor.setText("");
|
|
2464
|
+
this.cancelPrismPlan();
|
|
2465
|
+
return;
|
|
2466
|
+
}
|
|
2431
2467
|
if (text === "/reload") {
|
|
2432
2468
|
this.editor.setText("");
|
|
2433
2469
|
await this.handleReloadCommand();
|
|
@@ -2692,6 +2728,8 @@ export class InteractiveMode {
|
|
|
2692
2728
|
break;
|
|
2693
2729
|
case "agent_settled":
|
|
2694
2730
|
await this.checkShutdownRequested();
|
|
2731
|
+
void this.maybeShowUsageLimitRecoveryMenu(event);
|
|
2732
|
+
void this.maybeShowPrismPlanApprovalMenu(event);
|
|
2695
2733
|
break;
|
|
2696
2734
|
case "compaction_start": {
|
|
2697
2735
|
if (this.settingsManager.getShowTerminalProgress()) {
|
|
@@ -2790,6 +2828,189 @@ export class InteractiveMode {
|
|
|
2790
2828
|
}
|
|
2791
2829
|
}
|
|
2792
2830
|
}
|
|
2831
|
+
formatUsageLimitDuration(ms) {
|
|
2832
|
+
const totalMinutes = Math.ceil(ms / 60000);
|
|
2833
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
2834
|
+
const minutes = totalMinutes % 60;
|
|
2835
|
+
const parts = [];
|
|
2836
|
+
if (hours > 0) parts.push(hours + "h");
|
|
2837
|
+
if (minutes > 0 || parts.length === 0) parts.push(minutes + "min");
|
|
2838
|
+
return parts.join(" ");
|
|
2839
|
+
}
|
|
2840
|
+
humanizeUsageLimitError(errorMessage) {
|
|
2841
|
+
return String(errorMessage || "").replace(/(~|about\s*)?(\d+)\s*min\b/gi, (_match, prefix, minutes) => {
|
|
2842
|
+
return (prefix || "") + this.formatUsageLimitDuration(Number(minutes) * 60 * 1000);
|
|
2843
|
+
});
|
|
2844
|
+
}
|
|
2845
|
+
extractUsageLimitWaitMs(errorMessage) {
|
|
2846
|
+
const text = String(errorMessage || "");
|
|
2847
|
+
const parsedSeconds = text.match(/"(?:resets_in_seconds|X-Codex-Primary-Reset-After-Seconds|Retry-After)"\s*:?\s*"?(\d+)"?/i)?.[1];
|
|
2848
|
+
if (parsedSeconds) return Number(parsedSeconds) * 1000;
|
|
2849
|
+
const resetAt = text.match(/"(?:resets_at|X-Codex-Primary-Reset-At)"\s*:?\s*"?(\d+)"?/i)?.[1];
|
|
2850
|
+
if (resetAt) {
|
|
2851
|
+
const resetMs = Number(resetAt) * 1000 - Date.now();
|
|
2852
|
+
if (Number.isFinite(resetMs) && resetMs > 0) return resetMs;
|
|
2853
|
+
}
|
|
2854
|
+
const hours = text.match(/(?:~|about\s*)?(\d+)\s*h(?:our)?/i)?.[1];
|
|
2855
|
+
if (hours) return Number(hours) * 60 * 60 * 1000;
|
|
2856
|
+
const minutes = text.match(/(?:~|about\s*)?(\d+)\s*min/i)?.[1];
|
|
2857
|
+
if (minutes) return Number(minutes) * 60 * 1000;
|
|
2858
|
+
const seconds = text.match(/(?:~|about\s*)?(\d+)\s*s(?:ec|econd)?/i)?.[1];
|
|
2859
|
+
if (seconds) return Number(seconds) * 1000;
|
|
2860
|
+
return 5 * 60 * 1000;
|
|
2861
|
+
}
|
|
2862
|
+
getUsageLimitError(event) {
|
|
2863
|
+
const last = event?.messages?.[event.messages.length - 1];
|
|
2864
|
+
let errorMessage = last?.errorMessage || last?.content?.find?.((block) => block?.type === "text")?.text || "";
|
|
2865
|
+
if (last?.role !== "assistant" || last?.stopReason !== "error") return undefined;
|
|
2866
|
+
errorMessage = this.humanizeUsageLimitError(errorMessage);
|
|
2867
|
+
return /usage limit|try again in/i.test(errorMessage) ? String(errorMessage) : undefined;
|
|
2868
|
+
}
|
|
2869
|
+
removeLastAssistantErrorFromState() {
|
|
2870
|
+
const messages = this.agent.state.messages;
|
|
2871
|
+
const last = messages[messages.length - 1];
|
|
2872
|
+
if (last?.role === "assistant" && last?.stopReason === "error") this.agent.state.messages = messages.slice(0, -1);
|
|
2873
|
+
}
|
|
2874
|
+
async continueAfterUsageLimitRecovery() {
|
|
2875
|
+
try {
|
|
2876
|
+
await this.agent.continue();
|
|
2877
|
+
} catch (error) {
|
|
2878
|
+
this.showError(error instanceof Error ? error.message : String(error));
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2881
|
+
maybeShowUsageLimitRecoveryMenu(event) {
|
|
2882
|
+
const errorMessage = this.getUsageLimitError(event);
|
|
2883
|
+
if (!errorMessage) return;
|
|
2884
|
+
const delayMs = this.extractUsageLimitWaitMs(errorMessage);
|
|
2885
|
+
const waitLabel = "Wait " + this.formatUsageLimitDuration(delayMs) + " then retry";
|
|
2886
|
+
this.showSelector((done) => {
|
|
2887
|
+
const selector = new ExtensionSelectorComponent("Usage limit hit", [waitLabel, "Change model and retry", "Cancel"], async (selected) => {
|
|
2888
|
+
done();
|
|
2889
|
+
if (selected === "Cancel") return;
|
|
2890
|
+
this.removeLastAssistantErrorFromState();
|
|
2891
|
+
if (selected === waitLabel) {
|
|
2892
|
+
const endTime = Date.now() + delayMs;
|
|
2893
|
+
const updateCountdown = () => {
|
|
2894
|
+
const remaining = Math.max(0, endTime - Date.now());
|
|
2895
|
+
this.showStatus("Waiting " + this.formatUsageLimitDuration(remaining) + " for usage limit, then retrying...");
|
|
2896
|
+
this.ui.requestRender();
|
|
2897
|
+
};
|
|
2898
|
+
updateCountdown();
|
|
2899
|
+
const countdownInterval = setInterval(updateCountdown, 60000);
|
|
2900
|
+
setTimeout(() => { clearInterval(countdownInterval); void this.continueAfterUsageLimitRecovery(); }, delayMs);
|
|
2901
|
+
return;
|
|
2902
|
+
}
|
|
2903
|
+
this.showUsageLimitModelSelector();
|
|
2904
|
+
}, () => {
|
|
2905
|
+
done();
|
|
2906
|
+
this.ui.requestRender();
|
|
2907
|
+
}, { tui: this.ui });
|
|
2908
|
+
return { component: selector, focus: selector };
|
|
2909
|
+
});
|
|
2910
|
+
}
|
|
2911
|
+
showUsageLimitModelSelector(initialSearchInput) {
|
|
2912
|
+
this.showSelector((done) => {
|
|
2913
|
+
const selector = new ModelSelectorComponent(this.ui, this.session.model, this.settingsManager, this.session.modelRegistry, this.session.scopedModels, async (model) => {
|
|
2914
|
+
try {
|
|
2915
|
+
await this.session.setModel(model);
|
|
2916
|
+
this.footer.invalidate();
|
|
2917
|
+
this.updateEditorBorderColor();
|
|
2918
|
+
done();
|
|
2919
|
+
this.showStatus("Model: " + model.id + "; retrying...");
|
|
2920
|
+
void this.maybeWarnAboutAnthropicSubscriptionAuth(model);
|
|
2921
|
+
this.checkDaxnutsEasterEgg(model);
|
|
2922
|
+
void this.continueAfterUsageLimitRecovery();
|
|
2923
|
+
} catch (error) {
|
|
2924
|
+
done();
|
|
2925
|
+
this.showError(error instanceof Error ? error.message : String(error));
|
|
2926
|
+
}
|
|
2927
|
+
}, () => {
|
|
2928
|
+
done();
|
|
2929
|
+
this.ui.requestRender();
|
|
2930
|
+
}, initialSearchInput);
|
|
2931
|
+
return { component: selector, focus: selector };
|
|
2932
|
+
});
|
|
2933
|
+
}
|
|
2934
|
+
getPrismReadOnlyToolNames() {
|
|
2935
|
+
const readOnly = new Set(["read", "grep", "find", "ls", "obsidian_memory_read", "obsidian_memory_search", "obsidian_memory_list", "web_search", "web_fetch"]);
|
|
2936
|
+
return this.session.getActiveToolNames().filter((name) => readOnly.has(name));
|
|
2937
|
+
}
|
|
2938
|
+
setPrismPlanStatus(enabled) {
|
|
2939
|
+
this.prismPlanMode = enabled;
|
|
2940
|
+
this.setExtensionStatus("prism-mode", enabled ? "[Plan]" : undefined);
|
|
2941
|
+
this.footer.invalidate();
|
|
2942
|
+
}
|
|
2943
|
+
async startPrismPlanMode(task) {
|
|
2944
|
+
if (!this.prismPlanPreviousTools) this.prismPlanPreviousTools = this.session.getActiveToolNames();
|
|
2945
|
+
if (this.prismPlanLeafId === undefined) this.prismPlanLeafId = this.session.sessionManager.getLeafId();
|
|
2946
|
+
this.session.setActiveToolsByName(this.getPrismReadOnlyToolNames());
|
|
2947
|
+
this.setPrismPlanStatus(true);
|
|
2948
|
+
globalThis.__PRISM_MANIFEST_TIER_OVERRIDE__ = "plan";
|
|
2949
|
+
const prompt = "PLAN MODE ACTIVE. You must only inspect and plan. Do not modify files or run mutating commands. Ask concise clarification questions if needed. When ready, write the full implementation plan, then end with exactly: /plan:ready\n\nTask: " + (task || "Create an implementation plan for the user's next change.");
|
|
2950
|
+
await this.session.prompt(prompt);
|
|
2951
|
+
}
|
|
2952
|
+
getPrismLastAssistantText(event) {
|
|
2953
|
+
const last = event?.messages?.[event.messages.length - 1];
|
|
2954
|
+
if (last?.role !== "assistant") return "";
|
|
2955
|
+
return (last.content || []).map((block) => block?.type === "text" ? block.text || "" : "").join("\n");
|
|
2956
|
+
}
|
|
2957
|
+
maybeShowPrismPlanApprovalMenu(event) {
|
|
2958
|
+
if (!this.prismPlanMode) return;
|
|
2959
|
+
const text = this.getPrismLastAssistantText(event);
|
|
2960
|
+
if (!text.includes("/plan:ready")) return;
|
|
2961
|
+
this.prismPlanText = text.replace("/plan:ready", "").trim();
|
|
2962
|
+
this.showSelector((done) => {
|
|
2963
|
+
const selector = new ExtensionSelectorComponent("Plan ready", ["Approve -> make changes", "Reject -> add more context", "Cancel plan"], async (selected) => {
|
|
2964
|
+
done();
|
|
2965
|
+
if (selected.startsWith("Approve")) return void this.approvePrismPlan();
|
|
2966
|
+
if (selected.startsWith("Reject")) return this.rejectPrismPlan();
|
|
2967
|
+
this.cancelPrismPlan();
|
|
2968
|
+
}, () => {
|
|
2969
|
+
done();
|
|
2970
|
+
this.ui.requestRender();
|
|
2971
|
+
}, { tui: this.ui });
|
|
2972
|
+
return { component: selector, focus: selector };
|
|
2973
|
+
});
|
|
2974
|
+
}
|
|
2975
|
+
restorePrismPlanTools() {
|
|
2976
|
+
if (this.prismPlanPreviousTools) this.session.setActiveToolsByName(this.prismPlanPreviousTools);
|
|
2977
|
+
this.prismPlanPreviousTools = undefined;
|
|
2978
|
+
if (globalThis.__PRISM_MANIFEST_TIER_OVERRIDE__ === "plan") globalThis.__PRISM_MANIFEST_TIER_OVERRIDE__ = undefined;
|
|
2979
|
+
this.setPrismPlanStatus(false);
|
|
2980
|
+
}
|
|
2981
|
+
restorePrismPlanBranch() {
|
|
2982
|
+
if (this.prismPlanLeafId === undefined) return;
|
|
2983
|
+
if (this.prismPlanLeafId === null) {
|
|
2984
|
+
this.session.sessionManager.resetLeaf();
|
|
2985
|
+
} else {
|
|
2986
|
+
this.session.sessionManager.branch(this.prismPlanLeafId);
|
|
2987
|
+
}
|
|
2988
|
+
this.agent.state.messages = this.session.sessionManager.buildSessionContext().messages;
|
|
2989
|
+
this.prismPlanLeafId = undefined;
|
|
2990
|
+
}
|
|
2991
|
+
async approvePrismPlan() {
|
|
2992
|
+
if (!this.prismPlanMode) return this.showWarning("No active plan to approve.");
|
|
2993
|
+
const planText = this.prismPlanText || "";
|
|
2994
|
+
this.prismPlanText = undefined;
|
|
2995
|
+
this.restorePrismPlanBranch();
|
|
2996
|
+
this.restorePrismPlanTools();
|
|
2997
|
+
await this.session.prompt("Implement this plan:\n\n" + planText);
|
|
2998
|
+
}
|
|
2999
|
+
rejectPrismPlan() {
|
|
3000
|
+
if (!this.prismPlanMode) return this.showWarning("No active plan to reject.");
|
|
3001
|
+
this.prismPlanText = undefined;
|
|
3002
|
+
this.editor.setText("/plan ");
|
|
3003
|
+
this.showStatus("Add more context after /plan and submit.");
|
|
3004
|
+
this.ui.requestRender();
|
|
3005
|
+
}
|
|
3006
|
+
cancelPrismPlan() {
|
|
3007
|
+
if (!this.prismPlanMode) return this.showWarning("No active plan to cancel.");
|
|
3008
|
+
this.restorePrismPlanBranch();
|
|
3009
|
+
this.prismPlanText = undefined;
|
|
3010
|
+
this.restorePrismPlanTools();
|
|
3011
|
+
this.showStatus("Plan cancelled");
|
|
3012
|
+
this.ui.requestRender();
|
|
3013
|
+
}
|
|
2793
3014
|
/** Extract text content from a user message */
|
|
2794
3015
|
getUserMessageText(message) {
|
|
2795
3016
|
if (message.role !== "user")
|
|
@@ -202,7 +202,7 @@ export class CombinedAutocompleteProvider {
|
|
|
202
202
|
prefix: atPrefix,
|
|
203
203
|
};
|
|
204
204
|
}
|
|
205
|
-
if (
|
|
205
|
+
if (textBeforeCursor.startsWith("/")) {
|
|
206
206
|
const spaceIndex = textBeforeCursor.indexOf(" ");
|
|
207
207
|
if (spaceIndex === -1) {
|
|
208
208
|
const prefix = textBeforeCursor.slice(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@daniel156161/prism",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.92",
|
|
4
4
|
"description": "Prism-branded wrapper around pi that stores config in ~/.prism",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -27,9 +27,7 @@
|
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@earendil-works/pi-ai": "^0.84.2",
|
|
29
29
|
"@earendil-works/pi-coding-agent": "^0.84.2",
|
|
30
|
-
"eventsource": "^5.1.0",
|
|
31
30
|
"pi-mcp-adapter": "^2.26.0",
|
|
32
|
-
"pkce-challenge": "^6.0.0",
|
|
33
31
|
"typebox": "^1.3.14"
|
|
34
32
|
},
|
|
35
33
|
"bundledDependencies": [
|