@bacnh85/pi-serena 0.3.2 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +7 -0
  2. package/index.ts +88 -2
  3. package/package.json +1 -1
  4. package/worker.ts +117 -1
package/README.md CHANGED
@@ -37,6 +37,8 @@ After install or update, restart Pi or run `/reload` in an existing Pi session.
37
37
  - `serena_get_symbols_overview`
38
38
  - `serena_find_symbol`
39
39
  - `serena_find_referencing_symbols`
40
+ - `serena_find_declaration`
41
+ - `serena_find_implementations`
40
42
  - `serena_replace_symbol_body`
41
43
  - `serena_insert_before_symbol`
42
44
  - `serena_insert_after_symbol`
@@ -46,15 +48,20 @@ After install or update, restart Pi or run `/reload` in an existing Pi session.
46
48
  - `serena_replace_content`
47
49
  - `serena_restart_language_server`
48
50
  - `serena_get_current_config`
51
+ - `serena_get_diagnostics_for_file`
49
52
  - `serena_check_onboarding_performed`
50
53
  - `serena_onboarding`
51
54
  - `serena_list_memories`
52
55
  - `serena_read_memory`
53
56
  - `serena_write_memory`
57
+ - `serena_edit_memory`
58
+ - `serena_rename_memory`
54
59
  - `serena_delete_memory`
55
60
 
56
61
  All tool outputs are truncated to 50KB / 2000 lines to match Pi-friendly output limits. Most tools accept optional `timeout_ms`.
57
62
 
63
+ When a worker request exceeds the configured timeout, the Python bridge process is automatically killed and a fresh worker is started for the next call. This prevents cascading timeouts from blocking future requests.
64
+
58
65
  ### Pattern search
59
66
 
60
67
  Use the Pi-facing `pattern` field with `serena_search_for_pattern`:
package/index.ts CHANGED
@@ -100,6 +100,32 @@ const writeMemorySchema = Type.Object({
100
100
  content: Type.String({ description: "Memory content to write." }),
101
101
  });
102
102
 
103
+ const editMemorySchema = Type.Object({
104
+ ...controlSchema,
105
+ memory_name: Type.String({ description: "The name of the memory to edit." }),
106
+ needle: Type.String({ description: "The string or regex pattern to search for." }),
107
+ repl: Type.String({ description: "The replacement string (verbatim)." }),
108
+ mode: Type.Union([Type.Literal("literal"), Type.Literal("regex")], { description: "Either 'literal' or 'regex', specifying how needle is interpreted." }),
109
+ allow_multiple_occurrences: Type.Optional(Type.Boolean({ description: "Whether to allow replacing multiple occurrences. If false and multiple matches are found, returns an error." })),
110
+ });
111
+
112
+ const renameMemorySchema = Type.Object({
113
+ ...controlSchema,
114
+ old_name: Type.String({ description: "Current name of the memory to rename." }),
115
+ new_name: Type.String({ description: "New name for the memory. Use '/' to organize into topics." }),
116
+ });
117
+
118
+ const symbolRefSchema = Type.Object({
119
+ ...controlSchema,
120
+ name_path: Type.String({ description: "Exact symbol name path to look up." }),
121
+ relative_path: Type.String({ description: "File containing the symbol, relative to project root." }),
122
+ });
123
+
124
+ const diagnosticsSchema = Type.Object({
125
+ ...controlSchema,
126
+ relative_path: Type.String({ description: "File path relative to the Serena project root to get diagnostics for." }),
127
+ });
128
+
103
129
  const searchPatternSchema = Type.Object({
104
130
  ...controlSchema,
105
131
  pattern: Type.String({ description: "Pattern to search for." }),
@@ -506,6 +532,66 @@ export default function serenaToolsExtension(pi: ExtensionAPI) {
506
532
  },
507
533
  });
508
534
 
535
+ pi.registerTool({
536
+ name: "serena_edit_memory",
537
+ label: "Serena Edit Memory",
538
+ description: "Replace content matching a pattern in a Serena project memory using literal or regex mode.",
539
+ promptSnippet: "Edit a Serena project memory by replacing content matching a pattern",
540
+ promptGuidelines: ["Use serena_edit_memory for targeted edits to existing memories without rewriting the whole file."],
541
+ parameters: editMemorySchema,
542
+ async execute(_id, params, _signal, _onUpdate, ctx) {
543
+ return callSerena(ctx, "edit_memory", params, lockPathForProject(params));
544
+ },
545
+ });
546
+
547
+ pi.registerTool({
548
+ name: "serena_rename_memory",
549
+ label: "Serena Rename Memory",
550
+ description: "Rename or move a Serena project memory. Supports moving between project and global scope (e.g., renaming 'global/foo' to 'bar').",
551
+ promptSnippet: "Rename or move a Serena project memory",
552
+ promptGuidelines: ["Use serena_rename_memory to rename or reorganize memories, using '/' in the name to organize into topics."],
553
+ parameters: renameMemorySchema,
554
+ async execute(_id, params, _signal, _onUpdate, ctx) {
555
+ return callSerena(ctx, "rename_memory", params, lockPathForProject(params));
556
+ },
557
+ });
558
+
559
+ pi.registerTool({
560
+ name: "serena_find_declaration",
561
+ label: "Serena Find Declaration",
562
+ description: "Find the declaration/definition of a symbol using the language server backend.",
563
+ promptSnippet: "Find the declaration/definition of a symbol",
564
+ promptGuidelines: ["Use serena_find_declaration to navigate to the definition of a known symbol, e.g. to find where a function is defined."],
565
+ parameters: symbolRefSchema,
566
+ async execute(_id, params, _signal, _onUpdate, ctx) {
567
+ return callWorkerAction(ctx, "find_declaration", params);
568
+ },
569
+ });
570
+
571
+ pi.registerTool({
572
+ name: "serena_find_implementations",
573
+ label: "Serena Find Implementations",
574
+ description: "Find implementations of a symbol using the language server backend.",
575
+ promptSnippet: "Find symbols that implement the given symbol",
576
+ promptGuidelines: ["Use serena_find_implementations to locate implementations of interfaces, abstract methods, or base classes."],
577
+ parameters: symbolRefSchema,
578
+ async execute(_id, params, _signal, _onUpdate, ctx) {
579
+ return callWorkerAction(ctx, "find_implementations", params);
580
+ },
581
+ });
582
+
583
+ pi.registerTool({
584
+ name: "serena_get_diagnostics_for_file",
585
+ label: "Serena File Diagnostics",
586
+ description: "Get LSP diagnostics (errors, warnings, hints) for a file using the language server backend.",
587
+ promptSnippet: "Get diagnostics for a file from the language server",
588
+ promptGuidelines: ["Use serena_get_diagnostics_for_file to inspect compiler/IDE diagnostics for a specific file."],
589
+ parameters: diagnosticsSchema,
590
+ async execute(_id, params, _signal, _onUpdate, ctx) {
591
+ return callWorkerAction(ctx, "get_diagnostics_for_file", params);
592
+ },
593
+ });
594
+
509
595
  pi.registerCommand("serena-status", {
510
596
  description: "Show Serena worker status",
511
597
  handler: async (args, ctx) => {
@@ -542,7 +628,7 @@ export default function serenaToolsExtension(pi: ExtensionAPI) {
542
628
  return {
543
629
  systemPrompt:
544
630
  event.systemPrompt +
545
- "\n\nSerena semantic tools are available for code-symbol work. For finding symbols, references, declarations, implementations, and refactoring targets, prefer serena_find_symbol / serena_get_symbols_overview / serena_find_referencing_symbols over grep/read. Use grep/read for exact text searches, docs, configs, and non-code files.",
631
+ "\n\nSerena semantic tools are available for code-symbol work. For finding symbols, references, declarations, implementations, and refactoring targets, prefer serena_find_symbol / serena_get_symbols_overview / serena_find_referencing_symbols / serena_find_declaration / serena_find_implementations over grep/read. Use grep/read for exact text searches, docs, configs, and non-code files.",
546
632
  };
547
633
  });
548
634
 
@@ -569,7 +655,7 @@ export default function serenaToolsExtension(pi: ExtensionAPI) {
569
655
  {
570
656
  customType: "serena-reminder",
571
657
  content:
572
- "Reminder: you are reading code files instead of using Serena semantic tools. For symbols, references, declarations, implementations, and refactoring, use serena_find_symbol / serena_get_symbols_overview / serena_find_referencing_symbols instead of more grep/read calls. Keep using read/grep for docs, configs, non-code files, and exact text searches.",
658
+ "Reminder: you are reading code files instead of using Serena semantic tools. For symbols, references, declarations, implementations, and refactoring, use serena_find_symbol / serena_get_symbols_overview / serena_find_referencing_symbols / serena_find_declaration / serena_find_implementations instead of more grep/read calls. Keep using read/grep for docs, configs, non-code files, and exact text searches.",
573
659
  display: true,
574
660
  },
575
661
  { deliverAs: "steer", triggerTurn: true },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bacnh85/pi-serena",
3
- "version": "0.3.2",
3
+ "version": "0.4.1",
4
4
  "description": "Pi extension that provides Serena semantic code tools through a persistent worker.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
package/worker.ts CHANGED
@@ -187,9 +187,109 @@ def handle(req: dict[str, Any]) -> dict[str, Any]:
187
187
  return {"id": req_id, "ok": False, "tool": tool_name, "errorType": classify_error(result), "error": result, "result": result}
188
188
  return {"id": req_id, "ok": True, "tool": tool_name, "result": result}
189
189
 
190
+ if action in ("find_declaration", "find_implementations"):
191
+ return _handle_find_symbol_action(req_id, action, req)
192
+
193
+ if action == "get_diagnostics_for_file":
194
+ return _handle_diagnostics(req_id, req)
195
+
190
196
  raise ValueError(f"Unknown action: {action}")
191
197
 
192
198
 
199
+ def _get_symbol_retriever(agent: SerenaAgent):
200
+ from serena.symbol import LanguageServerSymbolRetriever
201
+ project = agent.get_active_project_or_raise()
202
+ return project, LanguageServerSymbolRetriever(project)
203
+
204
+
205
+ def _find_symbol_or_raise(retriever, name_path: str, relative_path: str):
206
+ return retriever.find_unique(
207
+ name_path, substring_matching=False, within_relative_path=relative_path
208
+ )
209
+
210
+
211
+ def _format_locations(locations) -> str:
212
+ import json
213
+ result = []
214
+ for loc in locations:
215
+ uri = loc.get("uri", "")
216
+ range_data = loc.get("range", {})
217
+ start = range_data.get("start", {})
218
+ end = range_data.get("end", {})
219
+ result.append({
220
+ "uri": uri,
221
+ "range": {
222
+ "start": {"line": start.get("line"), "character": start.get("character")},
223
+ "end": {"line": end.get("line"), "character": end.get("character")},
224
+ },
225
+ })
226
+ return json.dumps(result, indent=2)
227
+
228
+
229
+ def _handle_find_symbol_action(req_id, action: str, req: dict[str, Any]) -> dict[str, Any]:
230
+ project = str(req.get("project") or os.getcwd())
231
+ context = str(req.get("context") or "ide")
232
+ params = req.get("params") or {}
233
+ name_path = params.get("name_path")
234
+ relative_path = params.get("relative_path")
235
+ if not isinstance(name_path, str) or not name_path:
236
+ return {"id": req_id, "ok": False, "tool": action, "error": f"{action} requires string parameter 'name_path'"}
237
+ if not isinstance(relative_path, str) or not relative_path:
238
+ return {"id": req_id, "ok": False, "tool": action, "error": f"{action} requires string parameter 'relative_path'"}
239
+
240
+ agent = get_agent(project, context)
241
+ project_obj, retriever = _get_symbol_retriever(agent)
242
+ symbol = _find_symbol_or_raise(retriever, name_path, relative_path)
243
+
244
+ sym_rel_path = symbol.relative_path
245
+ sym_line = symbol.line
246
+ sym_col = symbol.column
247
+ if sym_rel_path is None or sym_line is None or sym_col is None:
248
+ return {"id": req_id, "ok": False, "tool": action, "error": f"Symbol {name_path} has no position info"}
249
+
250
+ ls_manager = project_obj.get_language_server_manager_or_raise()
251
+ lang_server = ls_manager.get_language_server(sym_rel_path)
252
+
253
+ if action == "find_declaration":
254
+ locations = lang_server.request_definition(sym_rel_path, sym_line, sym_col)
255
+ else:
256
+ locations = lang_server.request_implementation(sym_rel_path, sym_line, sym_col)
257
+
258
+ if not locations:
259
+ return {"id": req_id, "ok": True, "tool": action, "result": "No declarations found."}
260
+
261
+ result = _format_locations(locations)
262
+ return {"id": req_id, "ok": True, "tool": action, "result": result}
263
+
264
+
265
+ def _handle_diagnostics(req_id, req: dict[str, Any]) -> dict[str, Any]:
266
+ project = str(req.get("project") or os.getcwd())
267
+ context = str(req.get("context") or "ide")
268
+ params = req.get("params") or {}
269
+ relative_path = params.get("relative_path")
270
+ if not isinstance(relative_path, str) or not relative_path:
271
+ return {"id": req_id, "ok": False, "tool": "get_diagnostics_for_file", "error": "get_diagnostics_for_file requires string parameter 'relative_path'"}
272
+
273
+ import json as _json
274
+ import pathlib
275
+ from pathlib import PurePath
276
+
277
+ agent = get_agent(project, context)
278
+ project_obj = agent.get_active_project_or_raise()
279
+ ls_manager = project_obj.get_language_server_manager_or_raise()
280
+ lang_server = ls_manager.get_language_server(relative_path)
281
+
282
+ try:
283
+ uri = pathlib.Path(str(PurePath(lang_server.repository_root_path, relative_path))).as_uri()
284
+ result = lang_server.send.text_document_diagnostic({
285
+ "textDocument": {"uri": uri},
286
+ })
287
+ return {"id": req_id, "ok": True, "tool": "get_diagnostics_for_file", "result": _json.dumps(result, indent=2, default=str)}
288
+ except Exception as exc:
289
+ return {"id": req_id, "ok": True, "tool": "get_diagnostics_for_file", "result": _json.dumps({"note": "Diagnostics request completed with no issues or language server does not support textDocument/diagnostic", "detail": str(exc)})}
290
+
291
+
292
+
193
293
  def main() -> int:
194
294
  try:
195
295
  for line in sys.stdin:
@@ -272,7 +372,13 @@ export class SerenaWorkerClient {
272
372
  return new Promise((resolve, reject) => {
273
373
  const timer = setTimeout(() => {
274
374
  this.pending.delete(id);
275
- reject(new Error(`Serena worker request timed out: ${payload.action ?? "unknown"}`));
375
+ // Kill and reset worker so subsequent requests don't pile up.
376
+ // The next request() call triggers ensureStarted() which spawns a fresh worker.
377
+ this.killAndReset();
378
+ reject(new Error(
379
+ `Serena worker request timed out: ${payload.action ?? "unknown"}. ` +
380
+ `Worker has been restarted; retry if needed.`
381
+ ));
276
382
  }, timeoutMs);
277
383
  this.pending.set(id, { resolve, reject, timer });
278
384
  this.process!.stdin.write(`${JSON.stringify(request)}\n`);
@@ -353,6 +459,16 @@ export class SerenaWorkerClient {
353
459
  }
354
460
  }
355
461
 
462
+ private killAndReset(): void {
463
+ if (this.process) {
464
+ this.process.kill();
465
+ this.process = undefined;
466
+ this.onStatus?.(undefined);
467
+ }
468
+ this.failAll(new Error("Serena worker killed due to timeout, restarted"));
469
+ this.buffer = "";
470
+ }
471
+
356
472
  private failAll(error: Error): void {
357
473
  for (const [id, pending] of this.pending) {
358
474
  clearTimeout(pending.timer);