pi2web 0.2.0 → 0.2.6
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 +209 -18
- package/dist/activity.js +178 -0
- package/dist/agent-feed.js +133 -0
- package/dist/agent-templates.js +144 -0
- package/dist/agents.js +575 -97
- package/dist/auth.js +85 -20
- package/dist/collab/baseline.js +84 -0
- package/dist/collab/dispatcher.js +221 -0
- package/dist/collab/hub.js +2888 -0
- package/dist/collab/pi-extension.js +494 -0
- package/dist/collab/review-flow.js +459 -0
- package/dist/collab/routes.js +285 -0
- package/dist/collab/schemas.js +281 -0
- package/dist/collab/scoring-flow.js +336 -0
- package/dist/collab/similarity.js +15 -0
- package/dist/collab/store.js +918 -0
- package/dist/collab/types.js +137 -0
- package/dist/collab/validate.js +185 -0
- package/dist/drafts.js +50 -0
- package/dist/http/agent-routes.js +196 -0
- package/dist/http/login-limiter.js +53 -0
- package/dist/http/mail-settings-routes.js +53 -0
- package/dist/http/system-prompt-routes.js +52 -0
- package/dist/http/utils.js +43 -0
- package/dist/http/web-assets.js +63 -0
- package/dist/http/workspace-routes.js +58 -0
- package/dist/mail-notifier.js +95 -21
- package/dist/main.js +141 -17
- package/dist/mcp.js +132 -0
- package/dist/metadata-store.js +199 -56
- package/dist/paths.js +10 -0
- package/dist/sdk-backend.js +288 -49
- package/dist/server.js +558 -358
- package/dist/session-namer.js +163 -0
- package/dist/terminals.js +94 -27
- package/dist/timeline.js +185 -0
- package/dist/voice.js +196 -40
- package/dist/workspaces.js +91 -26
- package/dist/ws/agent-socket.js +256 -0
- package/dist/ws/terminal-socket.js +46 -0
- package/npm-shrinkwrap.json +3685 -0
- package/package.json +18 -6
- package/packages/edge-tts-server/src/remote_pi_edge_tts/__pycache__/__init__.cpython-312.pyc +0 -0
- package/packages/edge-tts-server/src/remote_pi_edge_tts/__pycache__/__main__.cpython-312.pyc +0 -0
- package/packages/protocol/openapi.yaml +8 -2
- package/packages/protocol/websocket.schema.json +94 -10
- package/web/activity-view.js +121 -0
- package/web/app.js +3770 -404
- package/web/backend-state.js +269 -0
- package/web/collab.html +95 -0
- package/web/collab.js +1080 -0
- package/web/hid-keyboard.js +453 -0
- package/web/index.html +14 -9
- package/web/styles.css +73 -0
- package/ANDROID_APP_FEATURES.md +0 -256
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { getAgentDir, hasTrustRequiringProjectResources, ProjectTrustStore } from '@earendil-works/pi-coding-agent';
|
|
4
|
+
const globalConfigPath = () => path.join(getAgentDir(), 'mcp.json');
|
|
5
|
+
const projectConfigPath = (cwd) => path.join(cwd, '.pi', 'mcp.json');
|
|
6
|
+
/** A project entry without transport fields only overrides settings of the global server. */
|
|
7
|
+
const isOverrideOnly = (config) => config.command === undefined && config.url === undefined && config.type === undefined;
|
|
8
|
+
function readConfig(file, errors) {
|
|
9
|
+
let text;
|
|
10
|
+
try {
|
|
11
|
+
text = readFileSync(file, 'utf8');
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
const value = JSON.parse(text)?.mcpServers;
|
|
18
|
+
if (!value || typeof value !== 'object')
|
|
19
|
+
return {};
|
|
20
|
+
// One malformed entry must not hide the servers next to it.
|
|
21
|
+
const servers = {};
|
|
22
|
+
for (const [name, config] of Object.entries(value)) {
|
|
23
|
+
if (config && typeof config === 'object' && !Array.isArray(config))
|
|
24
|
+
servers[name] = config;
|
|
25
|
+
else
|
|
26
|
+
errors.push(`${file}: “${name}” is not a server configuration`);
|
|
27
|
+
}
|
|
28
|
+
return servers;
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
errors.push(`${file}: ${error.message}`);
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function summarize(entry) {
|
|
36
|
+
const config = entry.config, http = typeof config.url === 'string';
|
|
37
|
+
return {
|
|
38
|
+
name: entry.name,
|
|
39
|
+
enabled: config.enabled !== false,
|
|
40
|
+
exposure: typeof config.exposure === 'string' ? config.exposure : 'codemode',
|
|
41
|
+
transport: http ? 'http' : 'stdio',
|
|
42
|
+
target: http
|
|
43
|
+
? String(config.url)
|
|
44
|
+
: [config.command, ...(Array.isArray(config.args) ? config.args : [])].filter(Boolean).join(' ').trim(),
|
|
45
|
+
description: typeof config.description === 'string' ? config.description : undefined,
|
|
46
|
+
scope: entry.scope,
|
|
47
|
+
source: entry.source,
|
|
48
|
+
override: entry.override
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function load(cwd) {
|
|
52
|
+
const errors = [], globalFile = globalConfigPath(), projectFile = projectConfigPath(cwd);
|
|
53
|
+
let projectTrusted = false, explicitlyTrusted = false;
|
|
54
|
+
// Pi only reads project files such as .pi/mcp.json after the project was trusted interactively.
|
|
55
|
+
// A project without trust-requiring resources is read, but writing one into it would make Pi ask
|
|
56
|
+
// for trust first, so project overrides are only written where trust was actually granted.
|
|
57
|
+
try {
|
|
58
|
+
explicitlyTrusted = new ProjectTrustStore(getAgentDir()).get(cwd) === true;
|
|
59
|
+
projectTrusted = explicitlyTrusted || !hasTrustRequiringProjectResources(cwd);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
/* Untrusted is the safe default. */
|
|
63
|
+
}
|
|
64
|
+
const globals = readConfig(globalFile, errors), project = projectTrusted ? readConfig(projectFile, errors) : {};
|
|
65
|
+
const entries = new Map();
|
|
66
|
+
for (const [name, config] of Object.entries(globals))
|
|
67
|
+
entries.set(name, { name, config, scope: 'global', source: globalFile });
|
|
68
|
+
for (const [name, config] of Object.entries(project)) {
|
|
69
|
+
const existing = entries.get(name);
|
|
70
|
+
if (existing && isOverrideOnly(config))
|
|
71
|
+
entries.set(name, { ...existing, config: { ...existing.config, ...config }, override: projectFile });
|
|
72
|
+
else
|
|
73
|
+
entries.set(name, { name, config, scope: 'project', source: projectFile });
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
entries: [...entries.values()],
|
|
77
|
+
globalFile,
|
|
78
|
+
projectFile: projectTrusted ? projectFile : undefined,
|
|
79
|
+
projectTrusted,
|
|
80
|
+
explicitlyTrusted,
|
|
81
|
+
errors
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/** Every configured MCP server for `cwd`, including disabled ones. */
|
|
85
|
+
export function listMcpServers(cwd) {
|
|
86
|
+
const config = load(cwd);
|
|
87
|
+
return {
|
|
88
|
+
servers: config.entries.map(summarize),
|
|
89
|
+
globalConfig: config.globalFile,
|
|
90
|
+
projectConfig: config.projectFile,
|
|
91
|
+
projectTrusted: config.projectTrusted,
|
|
92
|
+
errors: config.errors
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/** Write one server's `enabled` flag into `file`, keeping the rest of the file as it is. */
|
|
96
|
+
function writeEnabled(file, name, enabled, explicit = false) {
|
|
97
|
+
// Only a missing file starts a new one: a file that cannot be read or parsed must not be overwritten.
|
|
98
|
+
let text;
|
|
99
|
+
try {
|
|
100
|
+
text = readFileSync(file, 'utf8');
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
if (error.code !== 'ENOENT')
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
const document = text?.trim() ? JSON.parse(text) : {};
|
|
107
|
+
const servers = document.mcpServers && typeof document.mcpServers === 'object'
|
|
108
|
+
? document.mcpServers
|
|
109
|
+
: (document.mcpServers = {});
|
|
110
|
+
const current = servers[name] && typeof servers[name] === 'object' ? servers[name] : (servers[name] = {});
|
|
111
|
+
// `enabled: true` is the default, so it is dropped where the server itself is defined. A project
|
|
112
|
+
// override only changes what it states, so there the flag is always written out.
|
|
113
|
+
if (enabled && !explicit)
|
|
114
|
+
delete current.enabled;
|
|
115
|
+
else
|
|
116
|
+
current.enabled = enabled;
|
|
117
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
118
|
+
writeFileSync(file, `${JSON.stringify(document, null, 2)}\n`);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Enable or disable one server. A global server is changed with a project override when the project is
|
|
122
|
+
* trusted, which keeps the change local to this project, and in the global file otherwise.
|
|
123
|
+
*/
|
|
124
|
+
export function setMcpServerEnabled(cwd, name, enabled) {
|
|
125
|
+
const config = load(cwd);
|
|
126
|
+
const entry = config.entries.find(item => item.name === name);
|
|
127
|
+
if (!entry)
|
|
128
|
+
throw Object.assign(new Error('MCP server not found'), { code: 'MCP_SERVER_NOT_FOUND' });
|
|
129
|
+
const asOverride = entry.scope === 'global' && config.explicitlyTrusted && !!config.projectFile;
|
|
130
|
+
writeEnabled(asOverride ? config.projectFile : entry.source, name, enabled, asOverride);
|
|
131
|
+
return listMcpServers(cwd);
|
|
132
|
+
}
|
package/dist/metadata-store.js
CHANGED
|
@@ -1,8 +1,31 @@
|
|
|
1
1
|
import Database from 'better-sqlite3';
|
|
2
2
|
import { chmodSync, mkdirSync, statSync } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
import { METADATA_DB_FILE } from './paths.js';
|
|
5
|
+
const asTime = (value) => {
|
|
6
|
+
const time = value instanceof Date ? value.getTime() : typeof value === 'number' ? value : Date.parse(value ?? '');
|
|
7
|
+
return Number.isFinite(time) ? time : Date.now();
|
|
8
|
+
};
|
|
9
|
+
const parseTemplate = (value) => {
|
|
10
|
+
if (typeof value !== 'string' || !value)
|
|
11
|
+
return;
|
|
12
|
+
try {
|
|
13
|
+
const parsed = JSON.parse(value);
|
|
14
|
+
if (parsed &&
|
|
15
|
+
typeof parsed.id === 'string' &&
|
|
16
|
+
typeof parsed.systemPrompt === 'string' &&
|
|
17
|
+
(parsed.mode === 'replace' || parsed.mode === 'append'))
|
|
18
|
+
return {
|
|
19
|
+
id: parsed.id,
|
|
20
|
+
name: String(parsed.name ?? parsed.id),
|
|
21
|
+
mode: parsed.mode,
|
|
22
|
+
systemPrompt: parsed.systemPrompt
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
catch { }
|
|
26
|
+
return;
|
|
27
|
+
};
|
|
28
|
+
/** SQLite-backed pi2web metadata. Pi conversation JSONL files remain the source of truth. */
|
|
6
29
|
export class MetadataStore {
|
|
7
30
|
dataDir;
|
|
8
31
|
db;
|
|
@@ -12,7 +35,9 @@ export class MetadataStore {
|
|
|
12
35
|
constructor(dataDir) {
|
|
13
36
|
this.dataDir = dataDir;
|
|
14
37
|
}
|
|
15
|
-
get file() {
|
|
38
|
+
get file() {
|
|
39
|
+
return path.join(this.dataDir, METADATA_DB_FILE);
|
|
40
|
+
}
|
|
16
41
|
init() {
|
|
17
42
|
if (this.db)
|
|
18
43
|
return;
|
|
@@ -21,7 +46,7 @@ export class MetadataStore {
|
|
|
21
46
|
chmodSync(this.dataDir, 0o700);
|
|
22
47
|
}
|
|
23
48
|
catch { }
|
|
24
|
-
const db = this.db = new Database(this.file);
|
|
49
|
+
const db = (this.db = new Database(this.file));
|
|
25
50
|
try {
|
|
26
51
|
chmodSync(this.file, 0o600);
|
|
27
52
|
}
|
|
@@ -45,6 +70,8 @@ export class MetadataStore {
|
|
|
45
70
|
session_name TEXT,
|
|
46
71
|
cwd TEXT NOT NULL,
|
|
47
72
|
status TEXT NOT NULL,
|
|
73
|
+
profile TEXT NOT NULL DEFAULT 'default',
|
|
74
|
+
template TEXT,
|
|
48
75
|
created_at INTEGER NOT NULL,
|
|
49
76
|
last_active_at INTEGER NOT NULL,
|
|
50
77
|
archived_at INTEGER,
|
|
@@ -77,63 +104,179 @@ export class MetadataStore {
|
|
|
77
104
|
CREATE INDEX IF NOT EXISTS sessions_workspace_active_recent ON sessions(workspace_id,last_active_at DESC,session_id DESC) WHERE archived_at IS NULL;
|
|
78
105
|
CREATE INDEX IF NOT EXISTS sessions_cwd_active_recent ON sessions(cwd,last_active_at DESC,session_id DESC) WHERE archived_at IS NULL;
|
|
79
106
|
`);
|
|
80
|
-
|
|
107
|
+
// Additive migration: old normal Agents stay normal after an upgrade.
|
|
108
|
+
const agentColumns = db.prepare('PRAGMA table_info(agents)').all();
|
|
109
|
+
if (!agentColumns.some(column => column.name === 'profile'))
|
|
110
|
+
db.exec("ALTER TABLE agents ADD COLUMN profile TEXT NOT NULL DEFAULT 'default'");
|
|
111
|
+
// System prompt template snapshot (JSON). Agents created before templates existed use Pi's prompt.
|
|
112
|
+
if (!agentColumns.some(column => column.name === 'template'))
|
|
113
|
+
db.exec('ALTER TABLE agents ADD COLUMN template TEXT');
|
|
114
|
+
// Only stamp the initial version: later migrations (collaboration tables) raise it and must not be reset on restart.
|
|
115
|
+
if (Number(db.pragma('user_version', { simple: true })) < 1)
|
|
116
|
+
db.pragma('user_version = 1');
|
|
81
117
|
this.archivedAgents = new Set(db.prepare('SELECT id FROM agents WHERE archived_at IS NOT NULL').all().map(row => row.id));
|
|
82
118
|
}
|
|
83
|
-
get database() {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return;
|
|
87
|
-
|
|
119
|
+
get database() {
|
|
120
|
+
if (!this.db)
|
|
121
|
+
throw new Error('Metadata store is not initialized');
|
|
122
|
+
return this.db;
|
|
123
|
+
}
|
|
124
|
+
/** Shared handle so collaboration tables live in the same file and the same backup. */
|
|
125
|
+
get connection() {
|
|
126
|
+
return this.database;
|
|
127
|
+
}
|
|
128
|
+
getSetting(key) {
|
|
129
|
+
const row = this.database.prepare('SELECT value FROM settings WHERE key=?').get(key);
|
|
130
|
+
if (!row)
|
|
131
|
+
return;
|
|
132
|
+
try {
|
|
133
|
+
return JSON.parse(row.value);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
setSetting(key, value) {
|
|
140
|
+
this.database
|
|
141
|
+
.prepare('INSERT INTO settings(key,value,updated_at) VALUES(?,?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value,updated_at=excluded.updated_at')
|
|
142
|
+
.run(key, JSON.stringify(value), Date.now());
|
|
143
|
+
}
|
|
144
|
+
listWorkspaces() {
|
|
145
|
+
return this.database
|
|
146
|
+
.prepare('SELECT id,label,root_path,created_at FROM workspaces ORDER BY created_at,id')
|
|
147
|
+
.all().map(row => ({
|
|
148
|
+
id: row.id,
|
|
149
|
+
label: row.label,
|
|
150
|
+
rootPath: row.root_path,
|
|
151
|
+
createdAt: new Date(row.created_at).toISOString()
|
|
152
|
+
}));
|
|
153
|
+
}
|
|
154
|
+
saveWorkspace(workspace) {
|
|
155
|
+
this.database
|
|
156
|
+
.prepare(`INSERT INTO workspaces(id,label,root_path,created_at) VALUES(?,?,?,?) ON CONFLICT(id) DO UPDATE SET label=excluded.label,root_path=excluded.root_path`)
|
|
157
|
+
.run(workspace.id, workspace.label, workspace.rootPath, asTime(workspace.createdAt));
|
|
158
|
+
}
|
|
159
|
+
listAgents() {
|
|
160
|
+
return this.database
|
|
161
|
+
.prepare('SELECT * FROM agents WHERE archived_at IS NULL ORDER BY last_active_at DESC,id DESC')
|
|
162
|
+
.all().map(row => ({
|
|
163
|
+
agentId: row.id,
|
|
164
|
+
workspaceId: row.workspace_id,
|
|
165
|
+
sessionId: row.session_id,
|
|
166
|
+
sessionFile: row.session_file ?? undefined,
|
|
167
|
+
sessionName: row.session_name ?? undefined,
|
|
168
|
+
cwd: row.cwd,
|
|
169
|
+
status: row.status,
|
|
170
|
+
profile: row.profile === 'collab' ? 'collab' : 'default',
|
|
171
|
+
...(parseTemplate(row.template) ? { template: parseTemplate(row.template) } : {}),
|
|
172
|
+
createdAt: new Date(row.created_at).toISOString(),
|
|
173
|
+
lastActiveAt: new Date(row.last_active_at).toISOString()
|
|
174
|
+
}));
|
|
175
|
+
}
|
|
176
|
+
writeAgent(record, archivedAt) {
|
|
177
|
+
this.database
|
|
178
|
+
.prepare(
|
|
179
|
+
// `template` is written on insert only: an Agent's system prompt template never changes.
|
|
180
|
+
`INSERT INTO agents(id,workspace_id,session_id,session_file,session_name,cwd,status,profile,template,created_at,last_active_at,archived_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?) ON CONFLICT(id) DO UPDATE SET workspace_id=excluded.workspace_id,session_id=excluded.session_id,session_file=excluded.session_file,session_name=excluded.session_name,cwd=excluded.cwd,status=excluded.status,profile=excluded.profile,last_active_at=excluded.last_active_at,archived_at=excluded.archived_at`)
|
|
181
|
+
.run(record.agentId, record.workspaceId, record.sessionId, record.sessionFile ?? null, record.sessionName ?? null, record.cwd, record.status, record.profile ?? 'default', record.template ? JSON.stringify(record.template) : null, asTime(record.createdAt), asTime(record.lastActiveAt), archivedAt);
|
|
182
|
+
}
|
|
183
|
+
saveAgent(record) {
|
|
184
|
+
if (this.archivedAgents.has(record.agentId))
|
|
185
|
+
return;
|
|
186
|
+
this.pendingAgents.delete(record.agentId);
|
|
187
|
+
this.writeAgent(record, null);
|
|
188
|
+
}
|
|
189
|
+
scheduleAgent(record) {
|
|
190
|
+
if (this.archivedAgents.has(record.agentId))
|
|
191
|
+
return;
|
|
192
|
+
this.pendingAgents.set(record.agentId, { ...record });
|
|
193
|
+
if (!this.flushTimer)
|
|
194
|
+
this.flushTimer = setTimeout(() => this.flush(), 1000);
|
|
195
|
+
}
|
|
196
|
+
archiveAgent(record) {
|
|
197
|
+
this.archivedAgents.add(record.agentId);
|
|
198
|
+
this.pendingAgents.delete(record.agentId);
|
|
199
|
+
this.writeAgent(record, Date.now());
|
|
200
|
+
}
|
|
201
|
+
flush() {
|
|
202
|
+
if (this.flushTimer) {
|
|
203
|
+
clearTimeout(this.flushTimer);
|
|
204
|
+
this.flushTimer = undefined;
|
|
205
|
+
}
|
|
206
|
+
if (!this.pendingAgents.size)
|
|
207
|
+
return;
|
|
208
|
+
const records = [...this.pendingAgents.values()];
|
|
209
|
+
this.pendingAgents.clear();
|
|
210
|
+
this.database.transaction((items) => {
|
|
211
|
+
for (const item of items)
|
|
212
|
+
this.writeAgent(item, null);
|
|
213
|
+
})(records);
|
|
88
214
|
}
|
|
89
|
-
catch {
|
|
90
|
-
return;
|
|
91
|
-
} }
|
|
92
|
-
setSetting(key, value) { this.database.prepare('INSERT INTO settings(key,value,updated_at) VALUES(?,?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value,updated_at=excluded.updated_at').run(key, JSON.stringify(value), Date.now()); }
|
|
93
|
-
listWorkspaces() { return this.database.prepare('SELECT id,label,root_path,created_at FROM workspaces ORDER BY created_at,id').all().map(row => ({ id: row.id, label: row.label, rootPath: row.root_path, createdAt: new Date(row.created_at).toISOString() })); }
|
|
94
|
-
saveWorkspace(workspace) { this.database.prepare(`INSERT INTO workspaces(id,label,root_path,created_at) VALUES(?,?,?,?) ON CONFLICT(id) DO UPDATE SET label=excluded.label,root_path=excluded.root_path`).run(workspace.id, workspace.label, workspace.rootPath, asTime(workspace.createdAt)); }
|
|
95
|
-
listAgents() { return this.database.prepare('SELECT * FROM agents WHERE archived_at IS NULL ORDER BY last_active_at DESC,id DESC').all().map(row => ({ agentId: row.id, workspaceId: row.workspace_id, sessionId: row.session_id, sessionFile: row.session_file ?? undefined, sessionName: row.session_name ?? undefined, cwd: row.cwd, status: row.status, createdAt: new Date(row.created_at).toISOString(), lastActiveAt: new Date(row.last_active_at).toISOString() })); }
|
|
96
|
-
writeAgent(record, archivedAt) { this.database.prepare(`INSERT INTO agents(id,workspace_id,session_id,session_file,session_name,cwd,status,created_at,last_active_at,archived_at) VALUES(?,?,?,?,?,?,?,?,?,?) ON CONFLICT(id) DO UPDATE SET workspace_id=excluded.workspace_id,session_id=excluded.session_id,session_file=excluded.session_file,session_name=excluded.session_name,cwd=excluded.cwd,status=excluded.status,last_active_at=excluded.last_active_at,archived_at=excluded.archived_at`).run(record.agentId, record.workspaceId, record.sessionId, record.sessionFile ?? null, record.sessionName ?? null, record.cwd, record.status, asTime(record.createdAt), asTime(record.lastActiveAt), archivedAt); }
|
|
97
|
-
saveAgent(record) { if (this.archivedAgents.has(record.agentId))
|
|
98
|
-
return; this.pendingAgents.delete(record.agentId); this.writeAgent(record, null); }
|
|
99
|
-
scheduleAgent(record) { if (this.archivedAgents.has(record.agentId))
|
|
100
|
-
return; this.pendingAgents.set(record.agentId, { ...record }); if (!this.flushTimer)
|
|
101
|
-
this.flushTimer = setTimeout(() => this.flush(), 1000); }
|
|
102
|
-
archiveAgent(record) { this.archivedAgents.add(record.agentId); this.pendingAgents.delete(record.agentId); this.writeAgent(record, Date.now()); }
|
|
103
|
-
flush() { if (this.flushTimer) {
|
|
104
|
-
clearTimeout(this.flushTimer);
|
|
105
|
-
this.flushTimer = undefined;
|
|
106
|
-
} if (!this.pendingAgents.size)
|
|
107
|
-
return; const records = [...this.pendingAgents.values()]; this.pendingAgents.clear(); this.database.transaction((items) => { for (const item of items)
|
|
108
|
-
this.writeAgent(item, null); })(records); }
|
|
109
215
|
syncSessions(workspaceId, cwd, sessions) {
|
|
110
216
|
const db = this.database, upsert = db.prepare(`INSERT INTO sessions(session_id,session_file,workspace_id,cwd,name,parent_session_path,created_at,last_active_at,message_count,first_message,file_size,file_mtime,archived_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,NULL) ON CONFLICT(session_id) DO UPDATE SET session_file=excluded.session_file,workspace_id=excluded.workspace_id,cwd=excluded.cwd,name=excluded.name,parent_session_path=excluded.parent_session_path,last_active_at=excluded.last_active_at,message_count=excluded.message_count,first_message=excluded.first_message,file_size=excluded.file_size,file_mtime=excluded.file_mtime,archived_at=NULL`);
|
|
111
217
|
const remove = db.prepare('DELETE FROM sessions WHERE session_file=?');
|
|
112
|
-
db.transaction(() => {
|
|
113
|
-
paths
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
218
|
+
db.transaction(() => {
|
|
219
|
+
const paths = new Set();
|
|
220
|
+
for (const session of sessions) {
|
|
221
|
+
paths.add(session.path);
|
|
222
|
+
let size = 0, mtime = asTime(session.modified);
|
|
223
|
+
try {
|
|
224
|
+
const stat = statSync(session.path);
|
|
225
|
+
size = stat.size;
|
|
226
|
+
mtime = stat.mtimeMs;
|
|
227
|
+
}
|
|
228
|
+
catch { }
|
|
229
|
+
upsert.run(session.id, session.path, workspaceId, cwd, session.name ?? null, session.parentSessionPath ?? null, asTime(session.created), asTime(session.modified), session.messageCount, session.firstMessage, size, mtime);
|
|
119
230
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
231
|
+
for (const row of db
|
|
232
|
+
.prepare('SELECT session_file FROM sessions WHERE cwd=? AND archived_at IS NULL')
|
|
233
|
+
.all(cwd))
|
|
234
|
+
if (!paths.has(row.session_file))
|
|
235
|
+
remove.run(row.session_file);
|
|
236
|
+
})();
|
|
237
|
+
}
|
|
238
|
+
upsertSessionFromAgent(record, summary) {
|
|
239
|
+
const sessionFile = typeof summary.sessionFile === 'string' ? summary.sessionFile : record.sessionFile;
|
|
240
|
+
if (!sessionFile)
|
|
241
|
+
return;
|
|
242
|
+
let size = 0, mtime = asTime(record.lastActiveAt);
|
|
243
|
+
try {
|
|
244
|
+
const stat = statSync(sessionFile);
|
|
245
|
+
size = stat.size;
|
|
246
|
+
mtime = stat.mtimeMs;
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const sessionId = typeof summary.sessionId === 'string' ? summary.sessionId : record.sessionId, name = typeof summary.sessionName === 'string' ? summary.sessionName : record.sessionName, messageCount = Number(summary.stats?.totalMessages) || 0;
|
|
252
|
+
this.database
|
|
253
|
+
.prepare(`INSERT INTO sessions(session_id,session_file,workspace_id,cwd,name,created_at,last_active_at,message_count,first_message,file_size,file_mtime,archived_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,NULL) ON CONFLICT(session_id) DO UPDATE SET session_file=excluded.session_file,workspace_id=excluded.workspace_id,cwd=excluded.cwd,name=excluded.name,last_active_at=excluded.last_active_at,message_count=excluded.message_count,file_size=excluded.file_size,file_mtime=excluded.file_mtime,archived_at=NULL`)
|
|
254
|
+
.run(sessionId, sessionFile, record.workspaceId, record.cwd, name ?? null, asTime(record.createdAt), Math.max(asTime(record.lastActiveAt), mtime), messageCount, '', size, mtime);
|
|
255
|
+
}
|
|
256
|
+
listSessions(cwd, limit, offset = 0) {
|
|
257
|
+
const bounded = limit === undefined ? undefined : Math.min(200, Math.max(1, Math.trunc(limit)));
|
|
258
|
+
const sql = `SELECT session_file AS path,session_id AS id,cwd,name,parent_session_path AS parentSessionPath,created_at,last_active_at,message_count AS messageCount,first_message AS firstMessage FROM sessions WHERE cwd=? AND archived_at IS NULL ORDER BY last_active_at DESC,session_id DESC${bounded === undefined ? '' : ' LIMIT ? OFFSET ?'}`;
|
|
259
|
+
const rows = (bounded === undefined
|
|
260
|
+
? this.database.prepare(sql).all(cwd)
|
|
261
|
+
: this.database.prepare(sql).all(cwd, bounded, Math.max(0, Math.trunc(offset))));
|
|
262
|
+
return rows.map(row => ({
|
|
263
|
+
...row,
|
|
264
|
+
created: new Date(row.created_at).toISOString(),
|
|
265
|
+
modified: new Date(row.last_active_at).toISOString(),
|
|
266
|
+
allMessagesText: ''
|
|
267
|
+
}));
|
|
268
|
+
}
|
|
269
|
+
hasSession(cwd, sessionFile) {
|
|
270
|
+
return !!this.database
|
|
271
|
+
.prepare('SELECT 1 FROM sessions WHERE cwd=? AND session_file=? AND archived_at IS NULL')
|
|
272
|
+
.get(cwd, sessionFile);
|
|
273
|
+
}
|
|
274
|
+
close() {
|
|
275
|
+
if (!this.db)
|
|
276
|
+
return;
|
|
277
|
+
this.flush();
|
|
278
|
+
this.db.pragma('wal_checkpoint(TRUNCATE)');
|
|
279
|
+
this.db.close();
|
|
280
|
+
this.db = undefined;
|
|
281
|
+
}
|
|
139
282
|
}
|
package/dist/paths.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* pi2web keeps its own state next to Pi's (`~/.pi/agent`): `~/.pi/pi2web` holds the metadata database,
|
|
5
|
+
* pairing-code hashes and the shareable Agent templates. `PI2WEB_DIR` or `--data-dir` override it.
|
|
6
|
+
*/
|
|
7
|
+
export const defaultDataDir = () => process.env.PI2WEB_DIR || path.join(homedir(), '.pi', 'pi2web');
|
|
8
|
+
export const METADATA_DB_FILE = 'pi2web.db';
|
|
9
|
+
/** Shareable Agent templates: one Markdown file per template, see `agent-templates.ts`. */
|
|
10
|
+
export const AGENT_TEMPLATES_DIR = 'agent-templates';
|