@bacnh85/pi-serena 0.1.3 → 0.1.4

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 +26 -0
  2. package/index.ts +57 -11
  3. package/package.json +1 -1
  4. package/worker.ts +16 -0
package/README.md CHANGED
@@ -69,6 +69,32 @@ Use the Pi-facing `pattern` field with `serena_search_for_pattern`:
69
69
 
70
70
  The extension maps `pattern` to Serena's backend `substring_pattern` parameter internally, so users do not need to call the Serena implementation detail directly.
71
71
 
72
+ ### Content replacement
73
+
74
+ Use Serena's current `replace_content` API through Pi-facing fields:
75
+
76
+ ```json
77
+ {
78
+ "relative_path": "src/example.py",
79
+ "needle": "old text",
80
+ "repl": "new text",
81
+ "mode": "literal"
82
+ }
83
+ ```
84
+
85
+ For regex replacement, set `mode` to `regex` and provide a Python regular expression in `needle`:
86
+
87
+ ```json
88
+ {
89
+ "relative_path": "src/example.py",
90
+ "needle": "beginning.*?end",
91
+ "repl": "replacement",
92
+ "mode": "regex"
93
+ }
94
+ ```
95
+
96
+ The Pi bridge also implements `serena_get_current_config` and `serena_restart_language_server` directly, so they work even when Serena's same-named native tools are inactive in single-project or default contexts.
97
+
72
98
  ## Commands
73
99
 
74
100
  - `/serena-status [project]`
package/index.ts CHANGED
@@ -54,7 +54,8 @@ const referencingSchema = Type.Object({
54
54
  ...controlSchema,
55
55
  name_path: Type.String({ description: "Exact symbol name path from find_symbol." }),
56
56
  relative_path: Type.String({ description: "File containing the symbol, relative to project root." }),
57
- include_body: Type.Optional(Type.Boolean()),
57
+ include_kinds: Type.Optional(Type.Array(Type.Number({ description: "LSP symbol kind integers to include." }))),
58
+ exclude_kinds: Type.Optional(Type.Array(Type.Number({ description: "LSP symbol kind integers to exclude." }))),
58
59
  max_answer_chars: MAX_CHARS_PARAM,
59
60
  });
60
61
 
@@ -114,10 +115,13 @@ const searchPatternSchema = Type.Object({
114
115
  const replaceContentSchema = Type.Object({
115
116
  ...controlSchema,
116
117
  relative_path: Type.String({ description: "File path relative to the Serena project root." }),
117
- old_string: Type.Optional(Type.String({ description: "Exact text or regex to replace, depending on Serena version." })),
118
- new_string: Type.Optional(Type.String({ description: "Replacement text, depending on Serena version." })),
119
- content: Type.Optional(Type.String({ description: "Replacement content for Serena versions that use content-based replacement." })),
120
- regex: Type.Optional(Type.String({ description: "Regex pattern for Serena versions that use regex replacement." })),
118
+ needle: Type.Optional(Type.String({ description: "Exact text or regex pattern to replace." })),
119
+ repl: Type.Optional(Type.String({ description: "Replacement text." })),
120
+ mode: Type.Optional(Type.Union([Type.Literal("literal"), Type.Literal("regex")], { description: "Interpret needle as exact text or as a Python regex." })),
121
+ old_string: Type.Optional(Type.String({ description: "Deprecated alias for literal needle." })),
122
+ new_string: Type.Optional(Type.String({ description: "Deprecated alias for repl with old_string." })),
123
+ content: Type.Optional(Type.String({ description: "Deprecated alias for repl with regex." })),
124
+ regex: Type.Optional(Type.String({ description: "Deprecated alias for regex needle." })),
121
125
  allow_multiple_occurrences: Type.Optional(Type.Boolean({ description: "Allow replacing multiple matches when supported." })),
122
126
  });
123
127
 
@@ -148,6 +152,41 @@ export function normalizeSearchPatternParams(params: Record<string, unknown>): R
148
152
  return normalized;
149
153
  }
150
154
 
155
+ export function normalizeFindReferencesParams(params: Record<string, unknown>): Record<string, unknown> {
156
+ const normalized = { ...params };
157
+ delete normalized.include_body;
158
+ return normalized;
159
+ }
160
+
161
+ export function normalizeReplaceContentParams(params: Record<string, unknown>): Record<string, unknown> {
162
+ const normalized = { ...params };
163
+
164
+ if (normalized.needle === undefined) {
165
+ normalized.needle = normalized.old_string ?? normalized.regex;
166
+ }
167
+ if (normalized.repl === undefined) {
168
+ normalized.repl = normalized.new_string ?? normalized.content;
169
+ }
170
+ if (normalized.mode === undefined) {
171
+ if (normalized.regex !== undefined) normalized.mode = "regex";
172
+ else if (normalized.old_string !== undefined) normalized.mode = "literal";
173
+ }
174
+
175
+ delete normalized.old_string;
176
+ delete normalized.new_string;
177
+ delete normalized.content;
178
+ delete normalized.regex;
179
+
180
+ return normalized;
181
+ }
182
+
183
+ function validateReplaceContentParams(params: Record<string, unknown>): string | undefined {
184
+ if (typeof params.needle !== "string") return "serena_replace_content requires string parameter 'needle' (or deprecated alias 'old_string'/'regex').";
185
+ if (typeof params.repl !== "string") return "serena_replace_content requires string parameter 'repl' (or deprecated alias 'new_string'/'content').";
186
+ if (params.mode !== "literal" && params.mode !== "regex") return "serena_replace_content requires mode to be 'literal' or 'regex'.";
187
+ return undefined;
188
+ }
189
+
151
190
  function truncateText(text: string): string {
152
191
  const lines = text.split("\n");
153
192
  let output = lines.slice(0, OUTPUT_MAX_LINES).join("\n");
@@ -229,10 +268,10 @@ export default function serenaToolsExtension(pi: ExtensionAPI) {
229
268
  return worker;
230
269
  };
231
270
 
232
- const callSerena = async (ctx: any, tool: string, rawParams: Record<string, unknown>, lockPath?: string) => {
271
+ const callWorkerAction = async (ctx: any, action: string, rawParams: Record<string, unknown>, extraPayload: Record<string, unknown> = {}, lockPath?: string) => {
233
272
  const { project, context, timeoutMs, params } = stripControlParams(rawParams);
234
273
  const run = async () => {
235
- const response = await getWorker(ctx).request({ action: "call", project, context, tool, params }, timeoutMs);
274
+ const response = await getWorker(ctx).request({ action, project, context, params, ...extraPayload }, timeoutMs);
236
275
  return {
237
276
  content: [{ type: "text" as const, text: resultText(response) }],
238
277
  details: response,
@@ -241,6 +280,10 @@ export default function serenaToolsExtension(pi: ExtensionAPI) {
241
280
  return lockPath ? withFileMutationQueue(lockPath, run) : run();
242
281
  };
243
282
 
283
+ const callSerena = async (ctx: any, tool: string, rawParams: Record<string, unknown>, lockPath?: string) => {
284
+ return callWorkerAction(ctx, "call", rawParams, { tool }, lockPath);
285
+ };
286
+
244
287
  const lockPathForRelativeFile = (rawParams: Record<string, unknown>): string | undefined => {
245
288
  const project = normalizeProject(rawParams.project);
246
289
  return typeof rawParams.relative_path === "string" && rawParams.relative_path.trim()
@@ -314,7 +357,7 @@ export default function serenaToolsExtension(pi: ExtensionAPI) {
314
357
  promptGuidelines: ["Use serena_find_referencing_symbols before changing public behavior or renaming a symbol."],
315
358
  parameters: referencingSchema,
316
359
  async execute(_id, params, _signal, _onUpdate, ctx) {
317
- return callSerena(ctx, "find_referencing_symbols", params);
360
+ return callSerena(ctx, "find_referencing_symbols", normalizeFindReferencesParams(params));
318
361
  },
319
362
  });
320
363
 
@@ -398,7 +441,10 @@ export default function serenaToolsExtension(pi: ExtensionAPI) {
398
441
  promptGuidelines: ["Prefer symbol-aware Serena edit tools for whole symbols; use serena_replace_content for non-symbol scoped replacements supported by Serena."],
399
442
  parameters: replaceContentSchema,
400
443
  async execute(_id, params, _signal, _onUpdate, ctx) {
401
- return callSerena(ctx, "replace_content", params, lockPathForRelativeFile(params));
444
+ const normalized = normalizeReplaceContentParams(params);
445
+ const error = validateReplaceContentParams(normalized);
446
+ if (error) return { content: [{ type: "text" as const, text: `Error: ${error}` }], details: { ok: false, error } };
447
+ return callSerena(ctx, "replace_content", normalized, lockPathForRelativeFile(params));
402
448
  },
403
449
  });
404
450
 
@@ -410,7 +456,7 @@ export default function serenaToolsExtension(pi: ExtensionAPI) {
410
456
  promptGuidelines: ["Use serena_restart_language_server when Serena symbol retrieval appears stale or language-server diagnostics are stuck."],
411
457
  parameters: emptyToolSchema,
412
458
  async execute(_id, params, _signal, _onUpdate, ctx) {
413
- return callSerena(ctx, "restart_language_server", params);
459
+ return callWorkerAction(ctx, "restart_language_server", params);
414
460
  },
415
461
  });
416
462
 
@@ -422,7 +468,7 @@ export default function serenaToolsExtension(pi: ExtensionAPI) {
422
468
  promptGuidelines: ["Use serena_get_current_config when project activation, contexts, modes, or tool availability are unclear."],
423
469
  parameters: emptyToolSchema,
424
470
  async execute(_id, params, _signal, _onUpdate, ctx) {
425
- return callSerena(ctx, "get_current_config", params);
471
+ return callWorkerAction(ctx, "config", params);
426
472
  },
427
473
  });
428
474
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bacnh85/pi-serena",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
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
@@ -154,6 +154,22 @@ def handle(req: dict[str, Any]) -> dict[str, Any]:
154
154
  opened = bool(req.get("open")) and agent.open_dashboard()
155
155
  return {"id": req_id, "ok": True, "opened": opened, "dashboardUrl": agent.get_dashboard_url()}
156
156
 
157
+ if action == "config":
158
+ project = str(req.get("project") or os.getcwd())
159
+ context = str(req.get("context") or "ide")
160
+ agent = get_agent(project, context)
161
+ with contextlib.redirect_stdout(sys.stderr):
162
+ result = agent.get_current_config_overview()
163
+ return {"id": req_id, "ok": True, "tool": "get_current_config", "result": result}
164
+
165
+ if action == "restart_language_server":
166
+ project = str(req.get("project") or os.getcwd())
167
+ context = str(req.get("context") or "ide")
168
+ agent = get_agent(project, context)
169
+ with contextlib.redirect_stdout(sys.stderr):
170
+ agent.reset_language_server_manager()
171
+ return {"id": req_id, "ok": True, "tool": "restart_language_server", "result": "OK"}
172
+
157
173
  if action == "call":
158
174
  project = str(req.get("project") or os.getcwd())
159
175
  context = str(req.get("context") or "ide")