pi-web 0.13.0 → 0.13.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,9 +6,8 @@ export class RpcSession {
6
6
  opts;
7
7
  constructor(opts) {
8
8
  this.opts = opts;
9
- const parts = opts.piCmd.split(/\s+/);
10
- const cmd = parts[0];
11
- const args = [...parts.slice(1), '--mode', 'rpc'];
9
+ const cmd = opts.piCmd.command;
10
+ const args = [...opts.piCmd.args, '--mode', 'rpc'];
12
11
  this.proc = spawn(cmd, args, {
13
12
  cwd: opts.cwd,
14
13
  stdio: ['pipe', 'pipe', 'pipe'],
@@ -42,8 +42,8 @@ function parseAgent(value) {
42
42
  }
43
43
  function getAgentCommand(agent) {
44
44
  return agent === 'omp'
45
- ? 'npx -y @oh-my-pi/pi-coding-agent@latest'
46
- : 'npx -y @mariozechner/pi-coding-agent@latest';
45
+ ? { command: 'npx', args: ['-y', '@oh-my-pi/pi-coding-agent@latest'] }
46
+ : { command: 'npx', args: ['-y', '@mariozechner/pi-coding-agent@latest'] };
47
47
  }
48
48
  const AGENT = parseAgent(getArg('agent'));
49
49
  const PORT = parseInt(getArg('port') || '8192', 10);
@@ -59,6 +59,7 @@ const distDir = distDirCandidates.find((candidate) => existsSync(join(candidate,
59
59
  const htmlPath = join(distDir, 'index.html');
60
60
  const htmlCache = isDev || !existsSync(htmlPath) ? null : readFileSync(htmlPath, 'utf-8');
61
61
  const HOME_DIR = resolve(homedir() || '/');
62
+ const SESSION_ROOT = resolve(join(HOME_DIR, AGENT === 'omp' ? '.omp' : '.pi', 'agent', 'sessions'));
62
63
  function isWithinRoot(path, root) {
63
64
  const rel = relative(root, path);
64
65
  return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
@@ -103,6 +104,22 @@ function serveFile(filePath, res) {
103
104
  res.writeHead(200, { 'Content-Type': contentTypes[ext] || 'application/octet-stream' });
104
105
  createReadStream(filePath).pipe(res);
105
106
  }
107
+ function logServerError(context, error) {
108
+ console.error(`[pi-web] ${context}`, error);
109
+ }
110
+ function respondJson(res, statusCode, payload) {
111
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
112
+ res.end(JSON.stringify(payload));
113
+ }
114
+ function isValidSessionFilename(filename) {
115
+ return basename(filename) === filename && filename.endsWith('.jsonl');
116
+ }
117
+ function resolveSessionPath(cwd, filename) {
118
+ if (!isValidSessionFilename(filename))
119
+ return null;
120
+ const resolved = resolve(getSessionFilePath(cwd, filename, AGENT));
121
+ return isWithinRoot(resolved, SESSION_ROOT) ? resolved : null;
122
+ }
106
123
  const server = createServer((req, res) => {
107
124
  if (req.url === '/' || req.url === '/index.html') {
108
125
  if (!existsSync(htmlPath)) {
@@ -123,12 +140,11 @@ const server = createServer((req, res) => {
123
140
  ...session,
124
141
  ...getSessionRuntimeStatus(session.cwd, session.file),
125
142
  }));
126
- res.writeHead(200, { 'Content-Type': 'application/json' });
127
- res.end(JSON.stringify(sessionsWithRuntime));
143
+ respondJson(res, 200, sessionsWithRuntime);
128
144
  })
129
145
  .catch((err) => {
130
- res.writeHead(500, { 'Content-Type': 'application/json' });
131
- res.end(JSON.stringify({ error: String(err) }));
146
+ logServerError('failed to list sessions', err);
147
+ respondJson(res, 500, { error: 'failed to list sessions' });
132
148
  });
133
149
  return;
134
150
  }
@@ -137,12 +153,11 @@ const server = createServer((req, res) => {
137
153
  const cwd = url.searchParams.get('cwd');
138
154
  listFolders(cwd)
139
155
  .then((data) => {
140
- res.writeHead(200, { 'Content-Type': 'application/json' });
141
- res.end(JSON.stringify(data));
156
+ respondJson(res, 200, data);
142
157
  })
143
158
  .catch((err) => {
144
- res.writeHead(500, { 'Content-Type': 'application/json' });
145
- res.end(JSON.stringify({ error: String(err) }));
159
+ logServerError('failed to list folders', err);
160
+ respondJson(res, 500, { error: 'failed to list folders' });
146
161
  });
147
162
  return;
148
163
  }
@@ -151,31 +166,36 @@ const server = createServer((req, res) => {
151
166
  const cwd = url.searchParams.get('cwd');
152
167
  const filename = url.searchParams.get('filename');
153
168
  if (!cwd || !filename) {
154
- res.writeHead(400, { 'Content-Type': 'application/json' });
155
- res.end(JSON.stringify({ error: 'cwd and filename parameters required' }));
169
+ respondJson(res, 400, { error: 'cwd and filename parameters required' });
170
+ return;
171
+ }
172
+ const file = resolveSessionPath(cwd, filename);
173
+ if (!file) {
174
+ respondJson(res, 400, { error: 'invalid session path' });
156
175
  return;
157
176
  }
158
- const file = getSessionFilePath(cwd, filename, AGENT);
159
177
  if (req.method === 'DELETE') {
160
178
  try {
179
+ const managed = findManagedSessionByFile(cwd, filename);
180
+ if (managed) {
181
+ closeManagedSession(managed);
182
+ }
161
183
  unlinkSync(file);
162
- res.writeHead(200, { 'Content-Type': 'application/json' });
163
- res.end(JSON.stringify({ ok: true }));
184
+ respondJson(res, 200, { ok: true });
164
185
  }
165
186
  catch (err) {
166
- res.writeHead(500, { 'Content-Type': 'application/json' });
167
- res.end(JSON.stringify({ error: String(err) }));
187
+ logServerError(`failed to delete session ${file}`, err);
188
+ respondJson(res, 500, { error: 'failed to delete session' });
168
189
  }
169
190
  return;
170
191
  }
171
192
  readSessionMessages(file)
172
193
  .then((messages) => {
173
- res.writeHead(200, { 'Content-Type': 'application/json' });
174
- res.end(JSON.stringify(messages));
194
+ respondJson(res, 200, messages);
175
195
  })
176
196
  .catch((err) => {
177
- res.writeHead(500, { 'Content-Type': 'application/json' });
178
- res.end(JSON.stringify({ error: String(err) }));
197
+ logServerError(`failed to read session ${file}`, err);
198
+ respondJson(res, 500, { error: 'failed to read session' });
179
199
  });
180
200
  return;
181
201
  }
@@ -184,8 +204,8 @@ const server = createServer((req, res) => {
184
204
  const safePath = normalize(url.pathname)
185
205
  .replace(/^(\.\.[/\\])+/, '')
186
206
  .replace(/^[/\\]+/, '');
187
- const filePath = join(distDir, safePath);
188
- if (filePath.startsWith(distDir) && existsSync(filePath) && statSync(filePath).isFile()) {
207
+ const filePath = resolve(join(distDir, safePath));
208
+ if (isWithinRoot(filePath, distDir) && existsSync(filePath) && statSync(filePath).isFile()) {
189
209
  serveFile(filePath, res);
190
210
  return;
191
211
  }
@@ -266,6 +286,19 @@ function closeManagedSession(managed) {
266
286
  unregisterManagedSession(managed);
267
287
  managed.rpc.kill();
268
288
  }
289
+ function findManagedSessionByFile(cwd, sessionFile) {
290
+ const key = buildSessionKey(resolve(cwd), basename(sessionFile));
291
+ const direct = rpcSessions.get(key);
292
+ if (direct && !direct.isClosing)
293
+ return direct;
294
+ for (const managed of rpcSessions.values()) {
295
+ if (managed.isClosing)
296
+ continue;
297
+ if (managed.cwd === resolve(cwd) && managed.keys.has(key))
298
+ return managed;
299
+ }
300
+ return null;
301
+ }
269
302
  function cleanupIfIdle(managed) {
270
303
  if (managed.isClosing)
271
304
  return;
@@ -363,6 +396,7 @@ function createManagedSession(cwd, sessionFile, initialKey) {
363
396
  onError: (error) => {
364
397
  if (!managed)
365
398
  return;
399
+ logServerError(`rpc session error (${managed.cwd}${managed.sessionFile ? `/${managed.sessionFile}` : ''})`, error);
366
400
  broadcast(managed, { type: 'error', message: error });
367
401
  },
368
402
  onExit: (code) => {
@@ -70,7 +70,7 @@ export async function readSessionMessages(filePath) {
70
70
  try {
71
71
  for await (const line of rl) {
72
72
  const trimmed = line.trim();
73
- if (!trimmed || !trimmed.startsWith('{"type":"message"'))
73
+ if (!trimmed)
74
74
  continue;
75
75
  try {
76
76
  const entry = JSON.parse(trimmed);
@@ -140,36 +140,28 @@ async function readSessionHeader(filePath) {
140
140
  const trimmed = line.trim();
141
141
  if (!trimmed)
142
142
  continue;
143
- if (!header) {
144
- try {
145
- const parsed = JSON.parse(trimmed);
146
- if (parsed.type === 'session') {
147
- header = parsed;
148
- continue;
149
- }
143
+ try {
144
+ const parsed = JSON.parse(trimmed);
145
+ if (!header && parsed.type === 'session') {
146
+ header = parsed;
147
+ continue;
150
148
  }
151
- catch { }
152
- }
153
- if (trimmed.startsWith('{"type":"message"')) {
149
+ if (parsed.type !== 'message')
150
+ continue;
154
151
  messageCount++;
155
- if (!firstPrompt && trimmed.includes('"role":"user"')) {
156
- try {
157
- const msg = JSON.parse(trimmed);
158
- if (msg.message?.role === 'user') {
159
- const content = msg.message.content;
160
- if (typeof content === 'string') {
161
- firstPrompt = content.slice(0, 120);
162
- }
163
- else if (Array.isArray(content)) {
164
- const text = content.find((c) => c.type === 'text');
165
- if (text?.text)
166
- firstPrompt = text.text.slice(0, 120);
167
- }
168
- }
152
+ if (!firstPrompt && parsed.message?.role === 'user') {
153
+ const content = parsed.message.content;
154
+ if (typeof content === 'string') {
155
+ firstPrompt = content.slice(0, 120);
156
+ }
157
+ else if (Array.isArray(content)) {
158
+ const text = content.find((c) => c.type === 'text');
159
+ if (text?.text)
160
+ firstPrompt = text.text.slice(0, 120);
169
161
  }
170
- catch { }
171
162
  }
172
163
  }
164
+ catch { }
173
165
  }
174
166
  }
175
167
  finally {