@bacnh85/pi-serena 0.4.0 → 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 +6 -0
  2. package/index.ts +100 -2
  3. package/package.json +1 -1
  4. package/worker.ts +100 -0
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,10 +48,14 @@ 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`
52
+ - `serena_check_onboarding_performed`
49
53
  - `serena_onboarding`
50
54
  - `serena_list_memories`
51
55
  - `serena_read_memory`
52
56
  - `serena_write_memory`
57
+ - `serena_edit_memory`
58
+ - `serena_rename_memory`
53
59
  - `serena_delete_memory`
54
60
 
55
61
  All tool outputs are truncated to 50KB / 2000 lines to match Pi-friendly output limits. Most tools accept optional `timeout_ms`.
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." }),
@@ -434,6 +460,18 @@ export default function serenaToolsExtension(pi: ExtensionAPI) {
434
460
  },
435
461
  });
436
462
 
463
+ pi.registerTool({
464
+ name: "serena_check_onboarding_performed",
465
+ label: "Serena Check Onboarding",
466
+ description: "Check whether Serena onboarding memories exist for this project.",
467
+ promptSnippet: "Check whether project onboarding was already performed",
468
+ promptGuidelines: ["Use serena_check_onboarding_performed before relying on Serena project memories."],
469
+ parameters: emptyToolSchema,
470
+ async execute(_id, params, _signal, _onUpdate, ctx) {
471
+ return callSerena(ctx, "check_onboarding_performed", params);
472
+ },
473
+ });
474
+
437
475
  pi.registerTool({
438
476
  name: "serena_onboarding",
439
477
  label: "Serena Onboarding",
@@ -494,6 +532,66 @@ export default function serenaToolsExtension(pi: ExtensionAPI) {
494
532
  },
495
533
  });
496
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
+
497
595
  pi.registerCommand("serena-status", {
498
596
  description: "Show Serena worker status",
499
597
  handler: async (args, ctx) => {
@@ -530,7 +628,7 @@ export default function serenaToolsExtension(pi: ExtensionAPI) {
530
628
  return {
531
629
  systemPrompt:
532
630
  event.systemPrompt +
533
- "\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.",
534
632
  };
535
633
  });
536
634
 
@@ -557,7 +655,7 @@ export default function serenaToolsExtension(pi: ExtensionAPI) {
557
655
  {
558
656
  customType: "serena-reminder",
559
657
  content:
560
- "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.",
561
659
  display: true,
562
660
  },
563
661
  { deliverAs: "steer", triggerTurn: true },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bacnh85/pi-serena",
3
- "version": "0.4.0",
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: