@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.
- package/README.md +7 -0
- package/index.ts +88 -2
- package/package.json +1 -1
- 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
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
|
-
|
|
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);
|