@bacnh85/pi-serena 0.1.0 → 0.1.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.
package/README.md CHANGED
@@ -6,6 +6,12 @@ Note: Serena itself is a Python package. The TypeScript worker owns lifecycle, r
6
6
 
7
7
  ## Install
8
8
 
9
+ Install the published package from npm:
10
+
11
+ ```bash
12
+ pi install npm:@bacnh85/pi-serena
13
+ ```
14
+
9
15
  From this repository checkout, install only this extension package:
10
16
 
11
17
  ```bash
@@ -18,6 +24,8 @@ For local development from a checkout:
18
24
  pi -e ./extensions/pi-serena
19
25
  ```
20
26
 
27
+ The package manifest points Pi directly at `./index.ts`, so published npm installs and local installs load the same extension entrypoint.
28
+
21
29
  There is intentionally no repository-level Pi package. Install each extension from its own subdirectory, matching `extensions/pi-rtk` and future extensions.
22
30
 
23
31
  After install or update, restart Pi or run `/reload` in an existing Pi session.
package/index.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { Type } from "typebox";
3
+ import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
4
+ import path from "node:path";
3
5
  import { SerenaWorkerClient, type SerenaWorkerResponse } from "./worker";
4
6
 
5
7
  const DEFAULT_CONTEXT = "ide";
@@ -214,15 +216,27 @@ export default function serenaToolsExtension(pi: ExtensionAPI) {
214
216
  return worker;
215
217
  };
216
218
 
217
- const callSerena = async (ctx: any, tool: string, rawParams: Record<string, unknown>) => {
219
+ const callSerena = async (ctx: any, tool: string, rawParams: Record<string, unknown>, lockPath?: string) => {
218
220
  const { project, context, timeoutMs, params } = stripControlParams(rawParams);
219
- const response = await getWorker(ctx).request({ action: "call", project, context, tool, params }, timeoutMs);
220
- return {
221
- content: [{ type: "text" as const, text: resultText(response) }],
222
- details: response,
221
+ const run = async () => {
222
+ const response = await getWorker(ctx).request({ action: "call", project, context, tool, params }, timeoutMs);
223
+ return {
224
+ content: [{ type: "text" as const, text: resultText(response) }],
225
+ details: response,
226
+ };
223
227
  };
228
+ return lockPath ? withFileMutationQueue(lockPath, run) : run();
224
229
  };
225
230
 
231
+ const lockPathForRelativeFile = (rawParams: Record<string, unknown>): string | undefined => {
232
+ const project = normalizeProject(rawParams.project);
233
+ return typeof rawParams.relative_path === "string" && rawParams.relative_path.trim()
234
+ ? path.resolve(project, rawParams.relative_path)
235
+ : undefined;
236
+ };
237
+
238
+ const lockPathForProject = (rawParams: Record<string, unknown>): string => path.resolve(normalizeProject(rawParams.project));
239
+
226
240
  pi.registerTool({
227
241
  name: "serena_status",
228
242
  label: "Serena Status",
@@ -299,7 +313,7 @@ export default function serenaToolsExtension(pi: ExtensionAPI) {
299
313
  promptGuidelines: ["Use serena_replace_symbol_body only after serena_find_symbol identifies the exact symbol to replace."],
300
314
  parameters: replaceBodySchema,
301
315
  async execute(_id, params, _signal, _onUpdate, ctx) {
302
- return callSerena(ctx, "replace_symbol_body", params);
316
+ return callSerena(ctx, "replace_symbol_body", params, lockPathForRelativeFile(params));
303
317
  },
304
318
  });
305
319
 
@@ -311,7 +325,7 @@ export default function serenaToolsExtension(pi: ExtensionAPI) {
311
325
  promptGuidelines: ["Use serena_insert_before_symbol for symbol-adjacent code insertion after locating the target symbol."],
312
326
  parameters: insertSchema,
313
327
  async execute(_id, params, _signal, _onUpdate, ctx) {
314
- return callSerena(ctx, "insert_before_symbol", params);
328
+ return callSerena(ctx, "insert_before_symbol", params, lockPathForRelativeFile(params));
315
329
  },
316
330
  });
317
331
 
@@ -323,7 +337,7 @@ export default function serenaToolsExtension(pi: ExtensionAPI) {
323
337
  promptGuidelines: ["Use serena_insert_after_symbol for adding sibling/helper symbols after a located symbol."],
324
338
  parameters: insertSchema,
325
339
  async execute(_id, params, _signal, _onUpdate, ctx) {
326
- return callSerena(ctx, "insert_after_symbol", params);
340
+ return callSerena(ctx, "insert_after_symbol", params, lockPathForRelativeFile(params));
327
341
  },
328
342
  });
329
343
 
@@ -335,7 +349,7 @@ export default function serenaToolsExtension(pi: ExtensionAPI) {
335
349
  promptGuidelines: ["Use serena_rename_symbol for cross-file code renames after finding the exact symbol and references."],
336
350
  parameters: renameSchema,
337
351
  async execute(_id, params, _signal, _onUpdate, ctx) {
338
- return callSerena(ctx, "rename_symbol", params);
352
+ return callSerena(ctx, "rename_symbol", params, lockPathForProject(params));
339
353
  },
340
354
  });
341
355
 
@@ -347,7 +361,7 @@ export default function serenaToolsExtension(pi: ExtensionAPI) {
347
361
  promptGuidelines: ["Use serena_safe_delete_symbol instead of text deletion when remaining references matter."],
348
362
  parameters: safeDeleteSchema,
349
363
  async execute(_id, params, _signal, _onUpdate, ctx) {
350
- return callSerena(ctx, "safe_delete_symbol", params);
364
+ return callSerena(ctx, "safe_delete_symbol", params, lockPathForRelativeFile(params));
351
365
  },
352
366
  });
353
367
 
@@ -371,7 +385,7 @@ export default function serenaToolsExtension(pi: ExtensionAPI) {
371
385
  promptGuidelines: ["Prefer symbol-aware Serena edit tools for whole symbols; use serena_replace_content for non-symbol scoped replacements supported by Serena."],
372
386
  parameters: replaceContentSchema,
373
387
  async execute(_id, params, _signal, _onUpdate, ctx) {
374
- return callSerena(ctx, "replace_content", params);
388
+ return callSerena(ctx, "replace_content", params, lockPathForRelativeFile(params));
375
389
  },
376
390
  });
377
391
 
@@ -455,7 +469,7 @@ export default function serenaToolsExtension(pi: ExtensionAPI) {
455
469
  promptGuidelines: ["Use serena_write_memory after onboarding or when durable verified project knowledge should be available to Serena."],
456
470
  parameters: writeMemorySchema,
457
471
  async execute(_id, params, _signal, _onUpdate, ctx) {
458
- return callSerena(ctx, "write_memory", params);
472
+ return callSerena(ctx, "write_memory", params, lockPathForProject(params));
459
473
  },
460
474
  });
461
475
 
@@ -467,7 +481,7 @@ export default function serenaToolsExtension(pi: ExtensionAPI) {
467
481
  promptGuidelines: ["Use serena_delete_memory only when the user explicitly asks to remove a Serena memory."],
468
482
  parameters: memoryNameSchema,
469
483
  async execute(_id, params, _signal, _onUpdate, ctx) {
470
- return callSerena(ctx, "delete_memory", params);
484
+ return callSerena(ctx, "delete_memory", params, lockPathForProject(params));
471
485
  },
472
486
  });
473
487
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bacnh85/pi-serena",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Pi extension that provides Serena semantic code tools through a persistent worker.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -28,7 +28,7 @@
28
28
  ],
29
29
  "pi": {
30
30
  "extensions": [
31
- "."
31
+ "./index.ts"
32
32
  ]
33
33
  },
34
34
  "peerDependencies": {
package/worker.ts CHANGED
@@ -21,8 +21,8 @@ function piConfigDirs(): string[] {
21
21
  return process.env.PI_CODING_AGENT_DIR ? [process.env.PI_CODING_AGENT_DIR] : [path.join(os.homedir(), ".pi", "agent"), path.join(os.homedir(), ".pi", "agents")];
22
22
  }
23
23
 
24
- function loadEnv(cwd = process.cwd()): void {
25
- for (const file of [path.resolve(cwd, ".env.local"), path.resolve(cwd, ".env"), ...piConfigDirs().flatMap((dir) => [path.join(dir, ".env.local"), path.join(dir, ".env")])]) {
24
+ function loadEnv(): void {
25
+ for (const file of piConfigDirs().flatMap((dir) => [path.join(dir, ".env.local"), path.join(dir, ".env")])) {
26
26
  let text: string;
27
27
  try { text = fs.readFileSync(file, "utf8"); } catch (error: any) { if (error?.code === "ENOENT") continue; throw error; }
28
28
  for (const rawLine of text.split(/\r?\n/)) {