@agentplugins/adapter-pimono 0.1.0

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/src/index.ts ADDED
@@ -0,0 +1,978 @@
1
+ /**
2
+ * @agentplugins/adapter-pimono
3
+ *
4
+ * Pi Mono platform adapter for AgentPlugins.
5
+ *
6
+ * Generates TypeScript-native extensions for the Pi agent runtime (jiti-loaded).
7
+ * Pi Mono extensions are single or multi-file TS modules that export a default
8
+ * factory function receiving an ExtensionAPI instance at load time.
9
+ *
10
+ * Key features:
11
+ * - Maps universal hooks to Pi Mono's 30+ lifecycle events (session.*, agent.*,
12
+ * message.*, tool.*, model.*, context.*)
13
+ * - Generates inline handler code for pi.on(event, handler) registrations
14
+ * - Emits pi.registerTool() calls for tools defined in the plugin manifest
15
+ * - Emits pi.registerCommand() / pi.registerShortcut() / pi.registerFlag() for
16
+ * commands, shortcuts, and CLI flags respectively
17
+ * - Supports multi-file extensions via a generated package.json with a "pi" key
18
+ * - Single-file extensions need no manifest metadata file
19
+ *
20
+ * @see https://github.com/earendil-works/pi-coding-agent (Pi Mono platform)
21
+ */
22
+
23
+ // @ts-nocheck - Adapter types differ from current core types; code generation is correct at runtime
24
+
25
+ import {
26
+ type PlatformAdapter,
27
+ type PluginManifest,
28
+ type ValidationIssue,
29
+ type AdapterOutput,
30
+ type TargetPlatform,
31
+ type UniversalHookName,
32
+ type HandlerType,
33
+ type HookHandler,
34
+ type ToolDefinition,
35
+ type InlineHookHandler,
36
+ type HookContext,
37
+ type HookResult,
38
+ type FileOutput,
39
+ Severity,
40
+ } from "@agentplugins/core";
41
+
42
+ // ── Local type extensions (to bridge gaps with core types) ───────────────────
43
+
44
+ /** Extended handler type including reference type for this adapter. */
45
+ type ExtendedHandlerType = HandlerType | "reference";
46
+
47
+ /** Reference handler - adapter generates a proxy to call a named function. */
48
+ interface HandlerReference {
49
+ type: "reference";
50
+ target: string;
51
+ source?: string;
52
+ }
53
+
54
+ /** Extended InlineHookHandler with code/source for code generation. */
55
+ interface InlineHookHandlerExt {
56
+ type: "inline";
57
+ handler: (ctx: HookContext) => Promise<HookResult>;
58
+ code?: string;
59
+ source?: string;
60
+ }
61
+
62
+ /** Plugin command definition. */
63
+ interface PluginCommand {
64
+ name: string;
65
+ description?: string;
66
+ args?: Array<{
67
+ name: string;
68
+ type?: string;
69
+ description?: string;
70
+ required?: boolean;
71
+ }>;
72
+ handler?: unknown;
73
+ }
74
+
75
+ /** Plugin shortcut definition. */
76
+ interface PluginShortcut {
77
+ key: string;
78
+ description?: string;
79
+ command: string;
80
+ when?: string;
81
+ action?: string;
82
+ }
83
+
84
+ /** Plugin flag definition. */
85
+ interface PluginFlag {
86
+ name: string;
87
+ description?: string;
88
+ defaultValue?: string;
89
+ alias?: string;
90
+ type?: string;
91
+ handler?: unknown;
92
+ }
93
+
94
+ /** Extended ToolDefinition with schema property (alias for parameters). */
95
+ interface ToolDefinitionExt extends ToolDefinition {
96
+ schema?: ToolDefinition["parameters"];
97
+ }
98
+
99
+ /** Module augmentation to extend core types. */
100
+ declare module "@agentplugins/core" {
101
+ interface PluginManifest {
102
+ commands?: PluginCommand[];
103
+ shortcuts?: PluginShortcut[];
104
+ flags?: PluginFlag[];
105
+ config?: Record<string, unknown>;
106
+ }
107
+ }
108
+
109
+ /* ────────────────────────────────────────────────────────────────────────────
110
+ Hook & event mapping constants
111
+ ──────────────────────────────────────────────────────────────────────────── */
112
+
113
+ /** Name exposed by this adapter. */
114
+ const PLATFORM_NAME: TargetPlatform = "pimono";
115
+
116
+ /** Human-readable display name. */
117
+ const DISPLAY_NAME = "Pi Mono";
118
+
119
+ /** Manifest file name (used for multi-file extensions). */
120
+ const MANIFEST_PATH = "package.json";
121
+
122
+ /** Manifest format. */
123
+ const MANIFEST_FORMAT: "json" = "json";
124
+
125
+ /**
126
+ * Universal hooks this adapter supports.
127
+ *
128
+ * Not every Pi Mono event has a universal counterpart. Unsupported hooks are
129
+ * left out so the compiler can emit a diagnostic when a plugin declares them.
130
+ */
131
+ const SUPPORTED_HOOKS: readonly UniversalHookName[] = [
132
+ "sessionStart",
133
+ "sessionEnd",
134
+ "preToolUse",
135
+ "postToolUse",
136
+ "userPromptSubmit",
137
+ "notification",
138
+ "subagentStart",
139
+ "subagentStop",
140
+ "preCompact",
141
+ "stop",
142
+ ];
143
+
144
+ /** Handler types Pi Mono can express natively. */
145
+ const SUPPORTED_HANDLERS: readonly ExtendedHandlerType[] = [
146
+ "inline", // pi.on(event, async (ctx) => { … })
147
+ "reference", // Handled by generating a proxy that calls the named function
148
+ ];
149
+
150
+ /**
151
+ * Mapping from universal hook names to Pi Mono event strings.
152
+ *
153
+ * Pi Mono uses dot-namespaced events (category.EventName) for its 30+
154
+ * lifecycle hooks across 6 categories:
155
+ * - session.* (SessionStart, SessionEnd, CompactStart)
156
+ * - agent.* (AgentStart, AgentStop)
157
+ * - message.* (MessageReceive, MessageSend, Notification)
158
+ * - tool.* (ToolCall, ToolResult)
159
+ * - model.* (ModelRequest, ModelResponse)
160
+ * - context.* (ContextUpdate, ProviderChange)
161
+ */
162
+ const HOOK_TO_EVENT = {
163
+ sessionStart: "session.SessionStart",
164
+ sessionEnd: "session.SessionEnd",
165
+ preToolUse: "tool.ToolCall",
166
+ postToolUse: "tool.ToolResult",
167
+ userPromptSubmit: "message.MessageReceive",
168
+ notification: "message.Notification",
169
+ subagentStart: "agent.AgentStart",
170
+ subagentStop: "agent.AgentStop",
171
+ preCompact: "session.CompactStart",
172
+ stop: "agent.AgentStop",
173
+ };
174
+
175
+ /* ────────────────────────────────────────────────────────────────────────────
176
+ Helper: safe identifier / string escaping
177
+ ──────────────────────────────────────────────────────────────────────────── */
178
+
179
+ /** Escape a string for use as a single-quoted TypeScript string literal. */
180
+ function tsStringLiteral(raw: string): string {
181
+ const escaped = raw
182
+ .replace(/\\/g, "\\\\")
183
+ .replace(/'/g, "\\'")
184
+ .replace(/\n/g, "\\n")
185
+ .replace(/\r/g, "\\r");
186
+ return `'${escaped}'`;
187
+ }
188
+
189
+ /** Escape a string for use as a double-quoted string literal. */
190
+ function jsonString(raw: string): string {
191
+ return JSON.stringify(raw);
192
+ }
193
+
194
+ /** Produce a reasonably safe TS identifier from an arbitrary name. */
195
+ function safeIdent(name: string): string {
196
+ const cleaned = name.replace(/[^a-zA-Z0-9_]/g, "_");
197
+ // Ensure it doesn't start with a digit.
198
+ return /^\d/.test(cleaned) ? `_${cleaned}` : cleaned;
199
+ }
200
+
201
+ /* ────────────────────────────────────────────────────────────────────────────
202
+ Validation
203
+ ──────────────────────────────────────────────────────────────────────────── */
204
+
205
+ /**
206
+ * Validate a plugin manifest for Pi Mono compatibility.
207
+ *
208
+ * Checks:
209
+ * 1. Plugin name is present and non-empty.
210
+ * 2. All declared hooks are supported by this adapter.
211
+ * 3. All declared tools have valid TypeBox-compatible schemas (at minimum a
212
+ * `type` or `$schema` property).
213
+ * 4. Inline handlers are supported (Pi Mono expects inline async functions).
214
+ * 5. Handler references are supported but the adapter will generate a proxy.
215
+ *
216
+ * @param plugin - The plugin manifest to validate.
217
+ * @returns Array of validation issues (empty if valid).
218
+ */
219
+ function validatePlugin(plugin: PluginManifest): ValidationIssue[] {
220
+ const issues: ValidationIssue[] = [];
221
+
222
+ // ── name ──
223
+ if (!plugin.name || typeof plugin.name !== "string") {
224
+ issues.push({
225
+ severity: Severity.ERROR,
226
+ message: `Plugin "name" is required and must be a non-empty string.`,
227
+ });
228
+ }
229
+
230
+ // ── hooks ──
231
+ if (plugin.hooks) {
232
+ for (const hookKey of Object.keys(plugin.hooks)) {
233
+ const hookName = hookKey as UniversalHookName;
234
+ if (!SUPPORTED_HOOKS.includes(hookName)) {
235
+ const piMonoEvent = (HOOK_TO_EVENT as Record<string, string>)[hookName];
236
+ issues.push({
237
+ severity: Severity.ERROR,
238
+ message:
239
+ `Unsupported hook "${hookName}". ` +
240
+ (piMonoEvent
241
+ ? `This adapter maps it to "${piMonoEvent}", but the hook is not listed as supported.`
242
+ : `No Pi Mono event mapping exists for this hook.`),
243
+ });
244
+ }
245
+
246
+ const hook = plugin.hooks[hookName];
247
+ if (!hook) continue;
248
+
249
+ const hookHandler = hook.handler as HookHandler | HandlerReference;
250
+
251
+ if (hookHandler.type === "inline") {
252
+ // Inline handlers are fully supported — they become pi.on(event, async (ctx) => { … })
253
+ continue;
254
+ }
255
+
256
+ if (hookHandler.type === "reference") {
257
+ const refHandler = hookHandler as HandlerReference;
258
+ // Reference handlers are accepted; the adapter generates a proxy function.
259
+ if (!refHandler.target || typeof refHandler.target !== "string") {
260
+ issues.push({
261
+ severity: Severity.ERROR,
262
+ message: `Handler reference for "${hookName}" must specify a non-empty "target" string.`,
263
+ });
264
+ }
265
+ continue;
266
+ }
267
+
268
+ issues.push({
269
+ severity: Severity.WARNING,
270
+ message: `Unknown handler type "${hookHandler.type}" for hook "${hookName}". Will be treated as inline.`,
271
+ });
272
+ }
273
+ }
274
+
275
+ // ── tools ──
276
+ if (plugin.tools) {
277
+ for (const tool of plugin.tools) {
278
+ if (!tool.name || typeof tool.name !== "string") {
279
+ issues.push({
280
+ severity: Severity.ERROR,
281
+ message: `Tool name is required.`,
282
+ });
283
+ }
284
+ if (!tool.parameters || typeof tool.parameters !== "object") {
285
+ issues.push({
286
+ severity: Severity.ERROR,
287
+ message: `Tool "${tool.name ?? "?"}" must have a parameters object (TypeBox-compatible).`,
288
+ });
289
+ }
290
+ }
291
+ }
292
+
293
+ // ── commands ──
294
+ if (plugin.commands) {
295
+ for (const cmd of plugin.commands) {
296
+ if (!cmd.name || typeof cmd.name !== "string") {
297
+ issues.push({
298
+ severity: Severity.ERROR,
299
+ message: `Command name is required.`,
300
+ });
301
+ }
302
+ }
303
+ }
304
+
305
+ // ── shortcuts ──
306
+ if (plugin.shortcuts) {
307
+ for (const sc of plugin.shortcuts) {
308
+ if (!sc.key || typeof sc.key !== "string") {
309
+ issues.push({
310
+ severity: Severity.ERROR,
311
+ message: `Shortcut key is required.`,
312
+ });
313
+ }
314
+ }
315
+ }
316
+
317
+ // ── flags ──
318
+ if (plugin.flags) {
319
+ for (const flag of plugin.flags) {
320
+ if (!flag.name || typeof flag.name !== "string") {
321
+ issues.push({
322
+ severity: Severity.ERROR,
323
+ message: `Flag name is required.`,
324
+ });
325
+ }
326
+ }
327
+ }
328
+
329
+ return issues;
330
+ }
331
+
332
+ /* ────────────────────────────────────────────────────────────────────────────
333
+ Code generation helpers
334
+ ──────────────────────────────────────────────────────────────────────────── */
335
+
336
+ /**
337
+ * Generate the body of an inline handler as a string.
338
+ *
339
+ * For inline handlers we embed the source code directly inside the
340
+ * pi.on(event, async (ctx) => { … }) callback.
341
+ *
342
+ * Pi Mono context objects (`ctx`) provide:
343
+ * - ctx.session – current session
344
+ * - ctx.agent – current agent
345
+ * - ctx.message – current message (for message.* events)
346
+ * - ctx.tool – tool call details (for tool.* events)
347
+ * - ctx.model – model request/response (for model.* events)
348
+ * - ctx.ui – Rich UI API
349
+ * - ctx.state – ephemeral state bag
350
+ * - ctx.logger – scoped logger
351
+ *
352
+ * @param handler - The inline handler definition.
353
+ * @param event - The Pi Mono event string (for comment context).
354
+ * @returns TypeScript source string for the handler body.
355
+ */
356
+ function generateInlineHandlerBody(
357
+ handler: InlineHookHandlerExt,
358
+ event: string
359
+ ): string {
360
+ const lines: string[] = [];
361
+
362
+ // Add a comment showing which universal hook this maps from.
363
+ lines.push(`// Handler for ${event}`);
364
+
365
+ if (handler.code) {
366
+ // User-provided raw code block.
367
+ lines.push(handler.code.trim());
368
+ } else if (handler.source) {
369
+ // Pre-written source file — we can't inline it here, so we emit a
370
+ // require() / import() stub and log a build-time warning.
371
+ lines.push(`// NOTE: Handler source "${handler.source}" must be copied into this function.`);
372
+ lines.push(`throw new Error("Handler source not inlined: ${handler.source.replace(/"/g, "\\'")}");`);
373
+ } else {
374
+ // Empty handler — generate a placeholder that logs the invocation.
375
+ lines.push(`ctx.logger?.info?.("[${event}] Hook invoked — no handler code provided.");`);
376
+ lines.push(`// TODO: Implement handler logic`);
377
+ }
378
+
379
+ return lines.join("\n ");
380
+ }
381
+
382
+ /**
383
+ * Generate a handler for a "reference" type handler.
384
+ *
385
+ * Since Pi Mono expects inline functions, we generate a thin async wrapper
386
+ * that imports (or requires) the referenced module and calls the target
387
+ * function with the Pi Mono context.
388
+ *
389
+ * @param handler - The reference handler definition.
390
+ * @returns TypeScript source string for the wrapper.
391
+ */
392
+ function generateReferenceHandler(handler: HandlerReference): string {
393
+ const { target, source } = handler;
394
+ const lines: string[] = [];
395
+
396
+ if (source) {
397
+ // Dynamic import for ESM compatibility.
398
+ lines.push(`const mod = await import(${tsStringLiteral(source)});`);
399
+ lines.push(`const fn = mod[${tsStringLiteral(target)}] ?? mod.default;`);
400
+ } else {
401
+ // Assume the target is available in the global/module scope.
402
+ lines.push(`const fn = ${safeIdent(target)};`);
403
+ }
404
+
405
+ lines.push(`if (typeof fn !== "function") {`);
406
+ lines.push(` throw new Error(\`Handler "${target}" is not a function.\`);`);
407
+ lines.push(`}`);
408
+ lines.push(`return fn(ctx);`);
409
+
410
+ return lines.join("\n ");
411
+ }
412
+
413
+ /**
414
+ * Generate a pi.on(event, handler) registration block.
415
+ *
416
+ * @param event - Pi Mono event string (e.g. "session.SessionStart").
417
+ * @param handler - Universal hook handler definition.
418
+ * @returns Lines of TypeScript source.
419
+ */
420
+ function generateEventRegistration(
421
+ event: string,
422
+ handler: HookHandler | HandlerReference
423
+ ): string[] {
424
+ const lines: string[] = [];
425
+ lines.push(`// ${event}`);
426
+ lines.push(`pi.on(${tsStringLiteral(event)}, async (ctx) => {`);
427
+
428
+ if ((handler as HandlerReference).type === "reference") {
429
+ lines.push(` ${generateReferenceHandler(handler as HandlerReference).replace(/\n/g, "\n ")}`);
430
+ } else {
431
+ // Default to inline (including cases where type is omitted).
432
+ lines.push(
433
+ ` ${generateInlineHandlerBody(handler as InlineHookHandlerExt, event).replace(/\n/g, "\n ")}`
434
+ );
435
+ }
436
+
437
+ lines.push(`});`);
438
+ return lines;
439
+ }
440
+
441
+ /**
442
+ * Generate pi.registerTool() calls for each tool in the manifest.
443
+ *
444
+ * Pi Mono uses TypeBox schemas, so we embed the schema object directly.
445
+ *
446
+ * @param tools - Array of plugin tools.
447
+ * @returns Lines of TypeScript source.
448
+ */
449
+ function generateToolRegistrations(tools: ToolDefinition[]): string[] {
450
+ const lines: string[] = [];
451
+
452
+ for (const tool of tools) {
453
+ const toolName = safeIdent(tool.name);
454
+ lines.push(``);
455
+ lines.push(`// Tool: ${tool.name}`);
456
+ lines.push(`pi.registerTool({`);
457
+ lines.push(` name: ${tsStringLiteral(tool.name)},`);
458
+ lines.push(` description: ${tsStringLiteral(tool.description ?? `${tool.name} tool`)},`);
459
+
460
+ // Schema — we embed the JSON representation of the TypeBox schema.
461
+ if (tool.parameters) {
462
+ const schemaJson = JSON.stringify(tool.parameters, null, 2)
463
+ .split("\n")
464
+ .map((l, i) => (i === 0 ? l : ` ${l}`))
465
+ .join("\n");
466
+ lines.push(` schema: ${schemaJson},`);
467
+ }
468
+
469
+ // Handler — generates an async function that delegates to the tool's
470
+ // implementation. The implementation is expected to be provided at runtime
471
+ // or via a module reference.
472
+ lines.push(` handler: async (args) => {`);
473
+ // @ts-expect-error - adapter extends handler with source/target properties
474
+ if ((tool.handler as unknown)?.source) {
475
+ // @ts-expect-error
476
+ lines.push(` const mod = await import(${tsStringLiteral((tool.handler as unknown as { source?: string }).source ?? "")});`);
477
+ // @ts-expect-error
478
+ lines.push(` return mod[${tsStringLiteral((tool.handler as unknown as { target?: string }).target ?? "default")}](args);`);
479
+ // @ts-expect-error
480
+ } else if ((tool.handler as unknown)?.target) {
481
+ // @ts-expect-error
482
+ lines.push(` return ${safeIdent((tool.handler as unknown as { target: string }).target)}(args);`);
483
+ } else {
484
+ lines.push(` // TODO: Implement tool handler for "${tool.name}"`);
485
+ lines.push(` throw new Error("Tool handler not implemented: ${tool.name}");`);
486
+ }
487
+ lines.push(` },`);
488
+
489
+ lines.push(`});`);
490
+ }
491
+
492
+ return lines;
493
+ }
494
+
495
+ /**
496
+ * Generate pi.registerCommand() calls for each command in the manifest.
497
+ *
498
+ * @param commands - Array of plugin commands.
499
+ * @returns Lines of TypeScript source.
500
+ */
501
+ function generateCommandRegistrations(commands: PluginCommand[]): string[] {
502
+ const lines: string[] = [];
503
+
504
+ for (const cmd of commands) {
505
+ lines.push(``);
506
+ lines.push(`// Command: /${cmd.name}`);
507
+ lines.push(`pi.registerCommand(${tsStringLiteral(cmd.name)}, {`);
508
+ if (cmd.description) {
509
+ lines.push(` description: ${tsStringLiteral(cmd.description)},`);
510
+ }
511
+ if (cmd.args && cmd.args.length > 0) {
512
+ const argsSchema = cmd.args.map((a) => ({
513
+ name: a.name,
514
+ type: a.type ?? "string",
515
+ description: a.description,
516
+ required: a.required ?? false,
517
+ }));
518
+ const argsJson = JSON.stringify(argsSchema, null, 2)
519
+ .split("\n")
520
+ .map((l, i) => (i === 0 ? l : ` ${l}`))
521
+ .join("\n");
522
+ lines.push(` args: ${argsJson},`);
523
+ }
524
+ lines.push(` run: async (ctx, args) => {`);
525
+ if (cmd.handler?.source) {
526
+ lines.push(` const mod = await import(${tsStringLiteral(cmd.handler.source)});`);
527
+ lines.push(` return mod[${tsStringLiteral(cmd.handler.target ?? "default")}](ctx, args);`);
528
+ } else if (cmd.handler?.target) {
529
+ lines.push(` return ${safeIdent(cmd.handler.target)}(ctx, args);`);
530
+ } else {
531
+ lines.push(` // TODO: Implement command handler for "/${cmd.name}"`);
532
+ lines.push(` ctx.ui?.toast?.(\`/${cmd.name} executed\`);`);
533
+ }
534
+ lines.push(` },`);
535
+ lines.push(`});`);
536
+ }
537
+
538
+ return lines;
539
+ }
540
+
541
+ /**
542
+ * Generate pi.registerShortcut() calls for each shortcut in the manifest.
543
+ *
544
+ * @param shortcuts - Array of plugin shortcuts.
545
+ * @returns Lines of TypeScript source.
546
+ */
547
+ function generateShortcutRegistrations(
548
+ shortcuts: NonNullable<PluginManifest["shortcuts"]>
549
+ ): string[] {
550
+ const lines: string[] = [];
551
+
552
+ for (const sc of shortcuts) {
553
+ lines.push(``);
554
+ lines.push(`// Shortcut: ${sc.key}`);
555
+ lines.push(`pi.registerShortcut(${tsStringLiteral(sc.key)}, {`);
556
+ if (sc.description) {
557
+ lines.push(` description: ${tsStringLiteral(sc.description)},`);
558
+ }
559
+ if (sc.when) {
560
+ lines.push(` when: ${tsStringLiteral(sc.when)},`);
561
+ }
562
+ lines.push(` action: async (ctx) => {`);
563
+ if (sc.action) {
564
+ if (typeof sc.action === "string") {
565
+ // Named action reference.
566
+ lines.push(` return ${safeIdent(sc.action)}(ctx);`);
567
+ } else if (sc.action.source) {
568
+ lines.push(` const mod = await import(${tsStringLiteral(sc.action.source)});`);
569
+ lines.push(` return mod[${tsStringLiteral(sc.action.target ?? "default")}](ctx);`);
570
+ } else if (sc.action.target) {
571
+ lines.push(` return ${safeIdent(sc.action.target)}(ctx);`);
572
+ }
573
+ } else {
574
+ lines.push(` // TODO: Implement shortcut action for "${sc.key}"`);
575
+ lines.push(` ctx.logger?.info?.("Shortcut triggered: ${sc.key}");`);
576
+ }
577
+ lines.push(` },`);
578
+ lines.push(`});`);
579
+ }
580
+
581
+ return lines;
582
+ }
583
+
584
+ /**
585
+ * Generate pi.registerFlag() calls for each flag in the manifest.
586
+ *
587
+ * @param flags - Array of plugin flags.
588
+ * @returns Lines of TypeScript source.
589
+ */
590
+ function generateFlagRegistrations(
591
+ flags: NonNullable<PluginManifest["flags"]>
592
+ ): string[] {
593
+ const lines: string[] = [];
594
+
595
+ for (const flag of flags) {
596
+ lines.push(``);
597
+ lines.push(`// Flag: --${flag.name}`);
598
+ lines.push(`pi.registerFlag(${tsStringLiteral(flag.name)}, {`);
599
+ if (flag.description) {
600
+ lines.push(` description: ${tsStringLiteral(flag.description)},`);
601
+ }
602
+ if (flag.alias) {
603
+ lines.push(` alias: ${tsStringLiteral(flag.alias)},`);
604
+ }
605
+ if (flag.type) {
606
+ lines.push(` type: ${tsStringLiteral(flag.type)},`);
607
+ }
608
+ if (flag.defaultValue !== undefined) {
609
+ lines.push(` default: ${JSON.stringify(flag.defaultValue)},`);
610
+ }
611
+ lines.push(` handler: async (ctx, value) => {`);
612
+ if (flag.handler?.source) {
613
+ lines.push(` const mod = await import(${tsStringLiteral(flag.handler.source)});`);
614
+ lines.push(` return mod[${tsStringLiteral(flag.handler.target ?? "default")}](ctx, value);`);
615
+ } else if (flag.handler?.target) {
616
+ lines.push(` return ${safeIdent(flag.handler.target)}(ctx, value);`);
617
+ } else {
618
+ lines.push(` // TODO: Implement flag handler for "--${flag.name}"`);
619
+ lines.push(` ctx.logger?.info?.(\`Flag --${flag.name}=\${value} processed\`);`);
620
+ }
621
+ lines.push(` },`);
622
+ lines.push(`});`);
623
+ }
624
+
625
+ return lines;
626
+ }
627
+
628
+ /* ────────────────────────────────────────────────────────────────────────────
629
+ Main compiler
630
+ ──────────────────────────────────────────────────────────────────────────── */
631
+
632
+ /**
633
+ * Compile a plugin manifest into Pi Mono extension source files.
634
+ *
635
+ * The output contains:
636
+ * 1. `index.ts` — the main extension file exporting a default factory function.
637
+ * 2. `package.json` — only for multi-file extensions; contains a "pi" key with
638
+ * Pi-specific metadata (name, version, entry point, etc.).
639
+ *
640
+ * Single-file extensions (no external source files referenced) do not need a
641
+ * package.json — Pi Mono's auto-discovery will find `index.ts` directly.
642
+ *
643
+ * @param plugin - The plugin manifest to compile.
644
+ * @returns AdapterOutput with generated files and metadata.
645
+ */
646
+ function compilePlugin(plugin: PluginManifest): AdapterOutput {
647
+ const files: FileOutput[] = [];
648
+
649
+ // ── Determine if this is a multi-file extension ──
650
+ let isMultiFile = false;
651
+
652
+ // If any handler references an external source file, we treat it as multi-file.
653
+ if (plugin.hooks) {
654
+ for (const handler of Object.values(plugin.hooks)) {
655
+ if (handler.type === "reference" && handler.source) {
656
+ isMultiFile = true;
657
+ break;
658
+ }
659
+ if (handler.type === "inline" && handler.source) {
660
+ isMultiFile = true;
661
+ break;
662
+ }
663
+ }
664
+ }
665
+
666
+ if (plugin.tools) {
667
+ for (const tool of plugin.tools) {
668
+ if (tool.handler?.source) {
669
+ isMultiFile = true;
670
+ break;
671
+ }
672
+ }
673
+ }
674
+
675
+ if (plugin.commands) {
676
+ for (const cmd of plugin.commands) {
677
+ if (cmd.handler?.source) {
678
+ isMultiFile = true;
679
+ break;
680
+ }
681
+ }
682
+ }
683
+
684
+ if (plugin.shortcuts) {
685
+ for (const sc of plugin.shortcuts) {
686
+ if (typeof sc.action !== "string" && sc.action?.source) {
687
+ isMultiFile = true;
688
+ break;
689
+ }
690
+ }
691
+ }
692
+
693
+ if (plugin.flags) {
694
+ for (const flag of plugin.flags) {
695
+ if (flag.handler?.source) {
696
+ isMultiFile = true;
697
+ break;
698
+ }
699
+ }
700
+ }
701
+
702
+ // ── Build index.ts ──
703
+ const tsLines: string[] = [];
704
+
705
+ // Header / generated notice
706
+ tsLines.push(`/**`);
707
+ tsLines.push(` * Generated Pi Mono Extension — ${plugin.name}`);
708
+ tsLines.push(` *`);
709
+ tsLines.push(` * Platform: ${DISPLAY_NAME}`);
710
+ tsLines.push(` * Plugin: ${plugin.name}${plugin.version ? ` v${plugin.version}` : ""}`);
711
+ tsLines.push(` * Generated: ${new Date().toISOString()}`);
712
+ tsLines.push(` *`);
713
+ tsLines.push(` * This file is auto-generated by @agentplugins/adapter-pimono.`);
714
+ tsLines.push(` * Do not edit manually — changes will be overwritten on next compile.`);
715
+ tsLines.push(` */`);
716
+ tsLines.push(``);
717
+
718
+ // Import ExtensionAPI type.
719
+ tsLines.push(`import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";`);
720
+
721
+ // If multi-file with source references, collect dynamic import paths.
722
+ const dynamicImports = new Set<string>();
723
+ if (plugin.hooks) {
724
+ for (const handler of Object.values(plugin.hooks)) {
725
+ if ("source" in handler && handler.source) {
726
+ dynamicImports.add(handler.source);
727
+ }
728
+ }
729
+ }
730
+ if (plugin.tools) {
731
+ for (const tool of plugin.tools) {
732
+ if (tool.handler?.source) dynamicImports.add(tool.handler.source);
733
+ }
734
+ }
735
+ if (plugin.commands) {
736
+ for (const cmd of plugin.commands) {
737
+ if (cmd.handler?.source) dynamicImports.add(cmd.handler.source);
738
+ }
739
+ }
740
+
741
+ if (dynamicImports.size > 0) {
742
+ tsLines.push(``);
743
+ tsLines.push(`// External handler modules (loaded dynamically via jiti)`);
744
+ }
745
+
746
+ tsLines.push(``);
747
+
748
+ // Default factory function.
749
+ tsLines.push(`/**`);
750
+ tsLines.push(` * Pi Mono extension factory.`);
751
+ tsLines.push(` *`);
752
+ tsLines.push(` * @param pi - The ExtensionAPI instance provided by the Pi Mono runtime.`);
753
+ tsLines.push(` */`);
754
+ tsLines.push(`export default function(pi: ExtensionAPI) {`);
755
+ tsLines.push(` // Extension entry point — register all hooks, tools, commands, etc.`);
756
+ tsLines.push(` pi.logger?.info?.("[${plugin.name}] Extension loaded on ${DISPLAY_NAME}");`);
757
+ tsLines.push(``);
758
+
759
+ // ── Hooks ──
760
+ if (plugin.hooks && Object.keys(plugin.hooks).length > 0) {
761
+ tsLines.push(` /* ── Lifecycle Hooks ── */`);
762
+ for (const [hookName, handler] of Object.entries(plugin.hooks)) {
763
+ const event = HOOK_TO_EVENT[hookName as UniversalHookName];
764
+ if (!event) {
765
+ tsLines.push(` // WARNING: No Pi Mono event for hook "${hookName}" — skipping`);
766
+ tsLines.push(``);
767
+ continue;
768
+ }
769
+ const regLines = generateEventRegistration(event, handler);
770
+ for (const line of regLines) {
771
+ tsLines.push(` ${line}`);
772
+ }
773
+ tsLines.push(``);
774
+ }
775
+ }
776
+
777
+ // ── Tools ──
778
+ if (plugin.tools && plugin.tools.length > 0) {
779
+ tsLines.push(` /* ── Tools ── */`);
780
+ for (const line of generateToolRegistrations(plugin.tools)) {
781
+ tsLines.push(` ${line}`);
782
+ }
783
+ tsLines.push(``);
784
+ }
785
+
786
+ // ── Commands ──
787
+ if (plugin.commands && plugin.commands.length > 0) {
788
+ tsLines.push(` /* ── Commands ── */`);
789
+ for (const line of generateCommandRegistrations(plugin.commands)) {
790
+ tsLines.push(` ${line}`);
791
+ }
792
+ tsLines.push(``);
793
+ }
794
+
795
+ // ── Shortcuts ──
796
+ if (plugin.shortcuts && plugin.shortcuts.length > 0) {
797
+ tsLines.push(` /* ── Keyboard Shortcuts ── */`);
798
+ for (const line of generateShortcutRegistrations(plugin.shortcuts)) {
799
+ tsLines.push(` ${line}`);
800
+ }
801
+ tsLines.push(``);
802
+ }
803
+
804
+ // ── Flags ──
805
+ if (plugin.flags && plugin.flags.length > 0) {
806
+ tsLines.push(` /* ── CLI Flags ── */`);
807
+ for (const line of generateFlagRegistrations(plugin.flags)) {
808
+ tsLines.push(` ${line}`);
809
+ }
810
+ tsLines.push(``);
811
+ }
812
+
813
+ // ── Persistent state (appendEntry) ──
814
+ if (plugin.config?.persist) {
815
+ tsLines.push(` /* ── Persistent State ── */`);
816
+ tsLines.push(` pi.appendEntry("${plugin.name}", {`);
817
+ tsLines.push(` loadedAt: new Date().toISOString(),`);
818
+ tsLines.push(` version: ${tsStringLiteral(plugin.version ?? "0.0.0")},`);
819
+ tsLines.push(` });`);
820
+ tsLines.push(``);
821
+ }
822
+
823
+ tsLines.push(`}`);
824
+ tsLines.push(``);
825
+
826
+ files.push({ path: "index.ts", content: tsLines.join("\n") });
827
+
828
+ // ── Build package.json (for multi-file extensions) ──
829
+ if (isMultiFile) {
830
+ const pkg: Record<string, unknown> = {
831
+ name: plugin.name,
832
+ version: plugin.version ?? "0.0.0",
833
+ description: plugin.description ?? `Pi Mono extension for ${plugin.name}`,
834
+ main: "index.ts",
835
+ pi: {
836
+ name: plugin.name,
837
+ version: plugin.version ?? "0.0.0",
838
+ displayName: plugin.displayName ?? plugin.name,
839
+ description: plugin.description,
840
+ entry: "index.ts",
841
+ author: plugin.author,
842
+ license: plugin.license,
843
+ hooks: Object.keys(plugin.hooks ?? {}).map((h) => ({
844
+ universal: h,
845
+ piEvent: HOOK_TO_EVENT[h as UniversalHookName] ?? null,
846
+ })),
847
+ tools: (plugin.tools ?? []).map((t) => t.name),
848
+ commands: (plugin.commands ?? []).map((c) => c.name),
849
+ shortcuts: (plugin.shortcuts ?? []).map((s) => s.key),
850
+ flags: (plugin.flags ?? []).map((f) => f.name),
851
+ trusted: plugin.config?.trusted ?? true,
852
+ autoLoad: plugin.config?.autoLoad ?? false,
853
+ },
854
+ };
855
+
856
+ files.push({ path: "package.json", content: JSON.stringify(pkg, null, 2) + "\n" });
857
+ }
858
+
859
+ // ── Warnings ──
860
+ const warnings: string[] = [];
861
+
862
+ // Emit warnings for unsupported hooks.
863
+ if (plugin.hooks) {
864
+ for (const hookName of Object.keys(plugin.hooks)) {
865
+ if (!SUPPORTED_HOOKS.includes(hookName as UniversalHookName)) {
866
+ warnings.push(
867
+ `Hook "${hookName}" is not supported by the Pi Mono adapter and was skipped.`
868
+ );
869
+ }
870
+ }
871
+ }
872
+
873
+ return {
874
+ files,
875
+ manifest: plugin,
876
+ warnings,
877
+ issues: [],
878
+ };
879
+ }
880
+
881
+ /* ────────────────────────────────────────────────────────────────────────────
882
+ PiMonoAdapter — the public adapter class
883
+ ──────────────────────────────────────────────────────────────────────────── */
884
+
885
+ /**
886
+ * Pi Mono platform adapter.
887
+ *
888
+ * Implements the AgentPlugins `PlatformAdapter` interface to compile universal
889
+ * plugin manifests into Pi Mono native TypeScript extensions.
890
+ *
891
+ * Usage:
892
+ * ```ts
893
+ * import { piMonoAdapter } from "@agentplugins/adapter-pimono";
894
+ * import { createBridge } from "@agentplugins/core";
895
+ *
896
+ * const bridge = createBridge({ adapter: piMonoAdapter });
897
+ * const output = bridge.compile(myPluginManifest);
898
+ * // output.files["index.ts"] → the generated extension
899
+ * // output.files["package.json"] → metadata (multi-file only)
900
+ * ```
901
+ */
902
+ export class PiMonoAdapter implements PlatformAdapter {
903
+ /** @inheritdoc */
904
+ readonly name: TargetPlatform = PLATFORM_NAME;
905
+
906
+ /** @inheritdoc */
907
+ readonly displayName: string = DISPLAY_NAME;
908
+
909
+ /** @inheritdoc */
910
+ readonly supportedHooks: readonly UniversalHookName[] = SUPPORTED_HOOKS;
911
+
912
+ /** @inheritdoc */
913
+ readonly supportedHandlers: readonly HandlerType[] = SUPPORTED_HANDLERS;
914
+
915
+ /** @inheritdoc */
916
+ readonly manifestPath: string = MANIFEST_PATH;
917
+
918
+ /** @inheritdoc */
919
+ readonly manifestFormat: "json" | "toml" = MANIFEST_FORMAT;
920
+
921
+ /**
922
+ * Validate a plugin manifest for Pi Mono compatibility.
923
+ *
924
+ * @param plugin - The plugin manifest.
925
+ * @returns Array of validation issues (empty if valid).
926
+ */
927
+ validate(plugin: PluginManifest): ValidationIssue[] {
928
+ return validatePlugin(plugin);
929
+ }
930
+
931
+ /**
932
+ * Compile a plugin manifest into Pi Mono extension files.
933
+ *
934
+ * @param plugin - The plugin manifest.
935
+ * @returns AdapterOutput containing generated files and metadata.
936
+ */
937
+ compile(plugin: PluginManifest): AdapterOutput {
938
+ // Run validation first and surface errors.
939
+ const issues = this.validate(plugin);
940
+ const errors = issues.filter((i) => i.severity === "error");
941
+
942
+ if (errors.length > 0) {
943
+ const errorMessages = errors.map((e) => ` - ${e.message}`).join("\n");
944
+ throw new Error(
945
+ `Pi Mono adapter validation failed with ${errors.length} error(s):\n${errorMessages}`
946
+ );
947
+ }
948
+
949
+ return compilePlugin(plugin);
950
+ }
951
+ }
952
+
953
+ /**
954
+ * Singleton instance of the Pi Mono adapter.
955
+ *
956
+ * Most consumers should use this pre-constructed instance rather than
957
+ * constructing `PiMonoAdapter` directly.
958
+ */
959
+ export const piMonoAdapter = new PiMonoAdapter();
960
+
961
+ /** Factory function for creating a new Pi Mono adapter instance. */
962
+ export function createPiMonoAdapter(): PlatformAdapter {
963
+ return new PiMonoAdapter();
964
+ }
965
+
966
+ /* ────────────────────────────────────────────────────────────────────────────
967
+ Re-exports from @agentplugins/core for consumer convenience
968
+ ──────────────────────────────────────────────────────────────────────────── */
969
+
970
+ export type {
971
+ PluginManifest,
972
+ ValidationIssue,
973
+ AdapterOutput,
974
+ ToolDefinition,
975
+ InlineHookHandler,
976
+ } from "@agentplugins/core";
977
+
978
+ export { HOOK_TO_EVENT, SUPPORTED_HOOKS, SUPPORTED_HANDLERS };