agentf 0.4.6 → 0.5.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.
@@ -46,12 +46,13 @@ module Agentf
46
46
  }
47
47
  }.freeze
48
48
 
49
- def initialize(global_root: Dir.home, local_root: Dir.pwd, dry_run: false, verbose: false, install_deps: true)
49
+ def initialize(global_root: Dir.home, local_root: Dir.pwd, dry_run: false, verbose: false, install_deps: true, opencode_runtime: "mcp")
50
50
  @global_root = global_root
51
51
  @local_root = local_root
52
52
  @dry_run = dry_run
53
53
  @verbose = verbose
54
54
  @install_deps = install_deps
55
+ @opencode_runtime = opencode_runtime.to_s
55
56
  end
56
57
 
57
58
  def install(
@@ -86,7 +87,7 @@ module Agentf
86
87
  end
87
88
 
88
89
  # Optionally install dependencies for opencode helper package.json
89
- if provider.to_s == "opencode" && @install_deps
90
+ if provider.to_s == "opencode" && @install_deps && opencode_plugin_runtime?
90
91
  roots.each do |root|
91
92
  package_json_path = File.join(root, ".opencode/package.json")
92
93
  if @dry_run
@@ -171,23 +172,33 @@ module Agentf
171
172
  File.join(root, ".opencode/agents/agentf-orchestrator.md"),
172
173
  render_workflow_engine_manifest
173
174
  )
174
- writes << write_manifest(
175
- File.join(root, ".opencode/plugins/agentf-plugin.ts"),
176
- render_opencode_plugin
177
- )
178
- writes << write_manifest(
179
- File.join(root, ".opencode/tsconfig.json"),
180
- render_opencode_tsconfig
181
- )
182
- writes << write_package_json(root)
183
175
  writes << write_manifest(
184
176
  File.join(root, ".opencode/memory/agentf-redis-schema.md"),
185
177
  render_opencode_memory_schema
186
178
  )
187
179
  writes << write_opencode_json(root)
180
+ if opencode_plugin_runtime?
181
+ writes << write_manifest(
182
+ File.join(root, ".opencode/plugins/opencode-plugin.d.ts"),
183
+ render_opencode_plugin
184
+ )
185
+ writes << write_manifest(
186
+ File.join(root, ".opencode/plugins/agentf-plugin.ts"),
187
+ render_agentf_plugin
188
+ )
189
+ writes << write_manifest(
190
+ File.join(root, ".opencode/tsconfig.json"),
191
+ render_opencode_tsconfig
192
+ )
193
+ writes << write_package_json(root)
194
+ end
188
195
  writes
189
196
  end
190
197
 
198
+ def opencode_plugin_runtime?
199
+ @opencode_runtime == "plugin"
200
+ end
201
+
191
202
  def discover_agents
192
203
  Agentf::Agents.constants
193
204
  .map { |const| Agentf::Agents.const_get(const) }
@@ -223,46 +234,42 @@ module Agentf
223
234
  end
224
235
 
225
236
  def render_agent_manifest(klass, provider:)
226
- meta = {
227
- "name" => agent_identifier(klass),
228
- "description" => klass.description,
229
- "commands" => klass.commands,
230
- "memory" => klass.memory_concepts,
231
- "policy" => klass.policy_boundaries
232
- }
237
+ # Emit a minimal, stable manifest that acts as a pointer to the runtime
238
+ # tool implemented by the plugin/CLI. Keep filename and `name` stable so
239
+ # upgrades remain compatible with existing installs.
240
+ tool_name = agent_identifier(klass)
233
241
 
234
- <<~MARKDOWN
235
- #{meta.to_yaml}---
236
- #{klass.prompt}
237
-
238
- ## Core Mission
239
- #{klass.description}
242
+ # Build a short policy summary (guard nils and limit length)
243
+ pb = klass.respond_to?(:policy_boundaries) ? klass.policy_boundaries || {} : {}
244
+ always = Array(pb["always"]).join("; ")
245
+ ask_first = Array(pb["ask_first"]).join("; ")
246
+ never = Array(pb["never"]).join("; ")
240
247
 
241
- ## When To Use
242
- #{klass.when_to_use}
248
+ parts = []
249
+ parts << "Always: #{always}" unless always.to_s.strip.empty?
250
+ parts << "Ask first: #{ask_first}" unless ask_first.to_s.strip.empty?
251
+ parts << "Never: #{never}" unless never.to_s.strip.empty?
252
+ policy_summary = parts.join(" | ")
243
253
 
244
- ## Deliverables
245
- #{Array(klass.deliverables).map { |item| "- #{item}" }.join("\n")}
254
+ description = klass.respond_to?(:description) ? klass.description.to_s.strip : ""
246
255
 
247
- ## Working Style
248
- #{klass.working_style}
256
+ <<~MARKDOWN
257
+ ---
258
+ name: #{tool_name}
259
+ description: #{description}
260
+ ---
261
+ This manifest is a thin pointer. All runtime logic lives in the `#{tool_name}` tool.
249
262
 
250
- ## Memory Integration
251
- - Reads: #{Array(klass.memory_concepts["reads"]).join(", ")}
252
- - Writes: #{Array(klass.memory_concepts["writes"]).join(", ")}
253
- - Policy: #{klass.memory_concepts["policy"]}
263
+ IMPORTANT: Use the `#{tool_name}` tool for any filesystem, codebase, or memory actions.
264
+ The manifest contains only routing and a small policy summary — the tool is the
265
+ authoritative implementation.
254
266
 
255
- ## Memory Actions
256
- #{memory_actions_for(klass, provider: provider).join("\n")}
267
+ If the tool returns `confirmation_required: true`, ask the user whether to continue.
268
+ If they approve, rerun the same tool with `confirmedWrite=confirmed`. If they decline,
269
+ do not retry the write.
257
270
 
258
- ## Policy Boundaries
259
- - Always: #{Array(klass.policy_boundaries["always"]).join("; ")}
260
- - Ask first: #{Array(klass.policy_boundaries["ask_first"]).join("; ")}
261
- - Never: #{Array(klass.policy_boundaries["never"]).join("; ")}
262
- - Required inputs: #{Array(klass.policy_boundaries["required_inputs"]).join(", ")}
263
- - Required outputs: #{Array(klass.policy_boundaries["required_outputs"]).join(", ")}
271
+ Policy Summary: #{policy_summary}
264
272
 
265
- #{copilot_mcp_agent_section(provider: provider)}
266
273
  MARKDOWN
267
274
  end
268
275
 
@@ -319,20 +326,19 @@ module Agentf
319
326
  end
320
327
 
321
328
  def render_command_manifest(manifest, provider:)
322
- commands = Array(manifest.fetch("commands"))
323
- frontmatter = {
324
- "name" => command_identifier(manifest.fetch("name")),
325
- "description" => manifest.fetch("description"),
326
- "commands" => commands
327
- }
329
+ cmd_name = command_identifier(manifest.fetch("name"))
330
+ desc = manifest.fetch("description", "").to_s.strip
328
331
 
329
332
  <<~MARKDOWN
330
- #{frontmatter.to_yaml}---
331
- # Commands
333
+ ---
334
+ name: #{cmd_name}
335
+ description: #{desc}
336
+ ---
337
+ This is a thin command manifest that routes execution to the `#{cmd_name}` tool.
332
338
 
333
- #{commands.map { |command| "- #{command.fetch('name')}" }.join("\n")}
339
+ IMPORTANT: Do not embed runtime logic here. Invoke the `#{cmd_name}` tool to perform
340
+ any codebase or memory operations.
334
341
 
335
- #{copilot_mcp_command_section(manifest: manifest, provider: provider)}
336
342
  MARKDOWN
337
343
  end
338
344
 
@@ -417,13 +423,34 @@ module Agentf
417
423
  end
418
424
 
419
425
  def render_opencode_plugin
426
+ <<~'TYPESCRIPT'
427
+ declare module "@opencode-ai/plugin" {
428
+ export type Plugin = (input: any) => Promise<any>;
429
+
430
+ // Minimal `tool` factory type used by our plugin. Keep very loose to avoid
431
+ // coupling to the full SDK types in this repo.
432
+ export function tool(def: any): any;
433
+
434
+ export const schema: any;
435
+
436
+ export default {} as { tool: typeof tool; schema: any };
437
+ }
438
+ TYPESCRIPT
439
+ end
440
+
441
+ def render_agentf_plugin
420
442
  <<~'TYPESCRIPT'
421
443
  // tools:
422
444
  import { execFile } from "child_process";
423
445
  import { promisify } from "util";
424
- import path from "path";
425
- import { type Plugin, tool } from "@opencode-ai/plugin";
426
- import fs from "fs";
446
+ import * as path from "path";
447
+ // Avoid importing host SDK types directly to reduce coupling during local
448
+ // type-checks. Use a runtime require and loose `any` types here.
449
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
450
+ const _opencode_plugin: any = require("@opencode-ai/plugin");
451
+ const tool = _opencode_plugin.tool;
452
+ type Plugin = any;
453
+ import * as fs from "fs";
427
454
 
428
455
  const execFileAsync = promisify(execFile);
429
456
 
@@ -553,317 +580,414 @@ module Agentf
553
580
  });
554
581
 
555
582
  const text = stdout.toString().trim();
556
- return text || "{}";
583
+ if (!text) return {};
584
+
585
+ try {
586
+ return JSON.parse(text);
587
+ } catch (err) {
588
+ // If the CLI returned non-JSON, return a structured error object so callers
589
+ // can surface useful debugging info instead of crashing.
590
+ return { ok: false, _parse_error: String(err), _raw: text };
591
+ }
557
592
  }
558
593
 
559
- export const agentfPlugin: Plugin = async () => {
560
- await ensureAgentfPreflight(process.env.PWD || process.cwd());
594
+ // Lightweight frontmatter parser: extract YAML between leading `---` blocks
595
+ function parseFrontmatter(content: string): Record<string, string> {
596
+ const res: Record<string, string> = {};
597
+ const fmStart = content.indexOf("---");
598
+ if (fmStart === -1) return res;
599
+ const rest = content.slice(fmStart + 3);
600
+ const fmEndIdx = rest.indexOf("---");
601
+ if (fmEndIdx === -1) return res;
602
+ const block = rest.slice(0, fmEndIdx).trim();
603
+ const lines = block.split(/\r?\n/);
604
+ for (const line of lines) {
605
+ const m = line.match(/^\s*([A-Za-z0-9_\-]+)\s*:\s*(.+)\s*$/);
606
+ if (!m) continue;
607
+ let key = m[1];
608
+ let value = m[2];
609
+ // strip surrounding quotes
610
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
611
+ value = value.slice(1, -1);
612
+ }
613
+ res[key] = value;
614
+ }
615
+ return res;
616
+ }
561
617
 
562
- return {
563
- tool: {
564
- "agentf-code-glob": tool({
565
- description: "Find files using project glob patterns via Agentf code CLI.",
566
- args: {
567
- pattern: tool.schema.string().describe("Glob pattern, example: lib/**/*.rb"),
568
- types: tool.schema.array(tool.schema.string()).optional().describe("Optional file extensions"),
569
- },
570
- async execute(args, context) {
571
- const commandArgs = [];
572
- if (args.types?.length) {
573
- commandArgs.push(`--types=${args.types.join(",")}`);
574
- }
618
+ export const agentfPlugin = async (input: any) => {
619
+ const workspaceDir = input?.directory || process.env.PWD || process.cwd();
620
+ await ensureAgentfPreflight(workspaceDir);
621
+
622
+ // Build static tools map
623
+ const staticTools: Record<string, ReturnType<typeof tool>> = {
624
+ "agentf-code-glob": tool({
625
+ description: "Find files using project glob patterns via Agentf code CLI.",
626
+ args: {
627
+ pattern: tool.schema.string().describe("Glob pattern, example: lib/**/*.rb"),
628
+ types: tool.schema.array(tool.schema.string()).optional().describe("Optional file extensions"),
629
+ },
630
+ async execute(_args: any, context: any) {
631
+ const commandArgs = [];
632
+ if (_args.types?.length) {
633
+ commandArgs.push(`--types=${_args.types.join(",")}`);
634
+ }
635
+
636
+ return runAgentfCli(context.directory, "code", "glob", [_args.pattern, ...commandArgs]);
637
+ },
638
+ }),
639
+ "agentf-code-grep": tool({
640
+ description: "Search file contents via Agentf code CLI.",
641
+ args: {
642
+ pattern: tool.schema.string().describe("Regex/text to search"),
643
+ filePattern: tool.schema.string().optional().describe("Optional include pattern"),
644
+ context: tool.schema.number().int().min(0).max(20).optional().describe("Context lines"),
645
+ },
646
+ async execute(_args: any, context: any) {
647
+ const commandArgs = [];
648
+ if (_args.filePattern) commandArgs.push(`--file-pattern=${_args.filePattern}`);
649
+ if (Number.isInteger(_args.context)) commandArgs.push(`--context=${_args.context}`);
650
+
651
+ return runAgentfCli(context.directory, "code", "grep", [_args.pattern, ...commandArgs]);
652
+ },
653
+ }),
654
+ "agentf-code-tree": tool({
655
+ description: "Get directory tree data via Agentf code CLI.",
656
+ args: {
657
+ depth: tool.schema.number().int().min(1).max(10).optional().describe("Max traversal depth"),
658
+ },
659
+ async execute(_args: any, context: any) {
660
+ const depth = _args.depth ?? 3;
661
+ return runAgentfCli(context.directory, "code", "tree", [`--depth=${depth}`]);
662
+ },
663
+ }),
664
+ "agentf-code-related-files": tool({
665
+ description: "Find import and related file hints for a target file.",
666
+ args: {
667
+ targetFile: tool.schema.string().describe("Workspace-relative file path"),
668
+ },
669
+ async execute(_args: any, context: any) {
670
+ return runAgentfCli(context.directory, "code", "related", [_args.targetFile]);
671
+ },
672
+ }),
673
+ "agentf-memory-recent": tool({
674
+ description: "Get recent Agentf memories from Redis.",
675
+ args: {
676
+ limit: tool.schema.number().int().min(1).max(100).optional().describe("How many memories to return"),
677
+ },
678
+ async execute(_args: any, context: any) {
679
+ const limit = _args.limit ?? 10;
680
+ return runAgentfCli(context.directory, "memory", "recent", ["-n", String(limit)]);
681
+ },
682
+ }),
683
+ "agentf-memory-search": tool({
684
+ description: "Search Agentf memories by keyword.",
685
+ args: {
686
+ query: tool.schema.string().describe("Search query"),
687
+ limit: tool.schema.number().int().min(1).max(100).optional().describe("How many results to return"),
688
+ },
689
+ async execute(_args: any, context: any) {
690
+ const limit = _args.limit ?? 10;
691
+ return runAgentfCli(context.directory, "memory", "search", [_args.query, "-n", String(limit)]);
692
+ },
693
+ }),
694
+ "agentf-memory-by-tag": tool({
695
+ description: "Get Agentf memories by tag.",
696
+ args: {
697
+ tag: tool.schema.string().describe("Tag to filter by"),
698
+ limit: tool.schema.number().int().min(1).max(100).optional().describe("How many results to return"),
699
+ },
700
+ async execute(_args: any, context: any) {
701
+ const limit = _args.limit ?? 10;
702
+ return runAgentfCli(context.directory, "memory", "by-tag", [_args.tag, "-n", String(limit)]);
703
+ },
704
+ }),
705
+ "agentf-memory-by-agent": tool({
706
+ description: "Get Agentf memories by agent.",
707
+ args: {
708
+ agent: tool.schema.string().describe("Agent name"),
709
+ limit: tool.schema.number().int().min(1).max(100).optional().describe("How many results to return"),
710
+ },
711
+ async execute(_args: any, context: any) {
712
+ const limit = _args.limit ?? 10;
713
+ return runAgentfCli(context.directory, "memory", "by-agent", [_args.agent, "-n", String(limit)]);
714
+ },
715
+ }),
716
+ "agentf-memory-by-type": tool({
717
+ description: "Get Agentf memories by type.",
718
+ args: {
719
+ type: tool.schema.string().describe("Memory type (pitfall|lesson|success|business_intent|feature_intent)"),
720
+ limit: tool.schema.number().int().min(1).max(100).optional().describe("How many results to return"),
721
+ },
722
+ async execute(_args: any, context: any) {
723
+ const limit = _args.limit ?? 10;
724
+ return runAgentfCli(context.directory, "memory", "by-type", [_args.type, "-n", String(limit)]);
725
+ },
726
+ }),
727
+ "agentf-memory-tags": tool({
728
+ description: "List all unique memory tags.",
729
+ args: {},
730
+ async execute(_args: any, context: any) {
731
+ return runAgentfCli(context.directory, "memory", "tags", []);
732
+ },
733
+ }),
734
+ "agentf-memory-pitfalls": tool({
735
+ description: "List pitfall memories.",
736
+ args: { limit: tool.schema.number().int().min(1).max(100).optional() },
737
+ async execute(_args: any, context: any) {
738
+ const limit = _args.limit ?? 10;
739
+ return runAgentfCli(context.directory, "memory", "pitfalls", ["-n", String(limit)]);
740
+ },
741
+ }),
742
+ "agentf-memory-lessons": tool({
743
+ description: "List lesson memories.",
744
+ args: { limit: tool.schema.number().int().min(1).max(100).optional() },
745
+ async execute(_args: any, context: any) {
746
+ const limit = _args.limit ?? 10;
747
+ return runAgentfCli(context.directory, "memory", "lessons", ["-n", String(limit)]);
748
+ },
749
+ }),
750
+ "agentf-memory-successes": tool({
751
+ description: "List success memories.",
752
+ args: { limit: tool.schema.number().int().min(1).max(100).optional() },
753
+ async execute(_args: any, context: any) {
754
+ const limit = _args.limit ?? 10;
755
+ return runAgentfCli(context.directory, "memory", "successes", ["-n", String(limit)]);
756
+ },
757
+ }),
758
+ "agentf-memory-intents": tool({
759
+ description: "List intents (business, feature or both).",
760
+ args: { kind: tool.schema.string().optional(), limit: tool.schema.number().int().min(1).max(100).optional() },
761
+ async execute(_args: any, context: any) {
762
+ const limit = _args.limit ?? 10;
763
+ const kind = _args.kind ? String(_args.kind) : "";
764
+ const cmdArgs = kind ? [kind, "-n", String(limit)] : ["-n", String(limit)];
765
+ return runAgentfCli(context.directory, "memory", "intents", cmdArgs);
766
+ },
767
+ }),
768
+ "agentf-memory-business-intents": tool({
769
+ description: "List business intents.",
770
+ args: { limit: tool.schema.number().int().min(1).max(100).optional() },
771
+ async execute(_args: any, context: any) {
772
+ const limit = _args.limit ?? 10;
773
+ return runAgentfCli(context.directory, "memory", "business-intents", ["-n", String(limit)]);
774
+ },
775
+ }),
776
+ "agentf-memory-feature-intents": tool({
777
+ description: "List feature intents.",
778
+ args: { limit: tool.schema.number().int().min(1).max(100).optional() },
779
+ async execute(_args: any, context: any) {
780
+ const limit = _args.limit ?? 10;
781
+ return runAgentfCli(context.directory, "memory", "feature-intents", ["-n", String(limit)]);
782
+ },
783
+ }),
784
+ "agentf-memory-add-business-intent": tool({
785
+ description: "Store a business intent in Redis.",
786
+ args: {
787
+ title: tool.schema.string(),
788
+ description: tool.schema.string(),
789
+ tags: tool.schema.array(tool.schema.string()).optional(),
790
+ constraints: tool.schema.array(tool.schema.string()).optional(),
791
+ priority: tool.schema.number().int().optional(),
792
+ },
793
+ async execute(_args: any, context: any) {
794
+ const commandArgs = [_args.title, _args.description];
795
+ if (_args.tags?.length) commandArgs.push(`--tags=${_args.tags.join(",")}`);
796
+ if (_args.constraints?.length) commandArgs.push(`--constraints=${_args.constraints.join(";")}`);
797
+ if (Number.isInteger(_args.priority)) commandArgs.push(`--priority=${String(_args.priority)}`);
798
+ return runAgentfCli(context.directory, "memory", "add-business-intent", commandArgs);
799
+ },
800
+ }),
801
+ "agentf-memory-add-feature-intent": tool({
802
+ description: "Store a feature intent in Redis.",
803
+ args: {
804
+ title: tool.schema.string(),
805
+ description: tool.schema.string(),
806
+ tags: tool.schema.array(tool.schema.string()).optional(),
807
+ acceptance: tool.schema.array(tool.schema.string()).optional(),
808
+ non_goals: tool.schema.array(tool.schema.string()).optional(),
809
+ related_task_id: tool.schema.string().optional(),
810
+ },
811
+ async execute(_args: any, context: any) {
812
+ const commandArgs = [_args.title, _args.description];
813
+ if (_args.tags?.length) commandArgs.push(`--tags=${_args.tags.join(",")}`);
814
+ if (_args.acceptance?.length) commandArgs.push(`--acceptance=${_args.acceptance.join(";")}`);
815
+ if (_args.non_goals?.length) commandArgs.push(`--non-goals=${_args.non_goals.join(";")}`);
816
+ if (_args.related_task_id) commandArgs.push(`--task=${_args.related_task_id}`);
817
+ return runAgentfCli(context.directory, "memory", "add-feature-intent", commandArgs);
818
+ },
819
+ }),
820
+ "agentf-memory-neighbors": tool({
821
+ description: "Get neighboring memory nodes by edge traversal.",
822
+ args: {
823
+ node_id: tool.schema.string(),
824
+ relation: tool.schema.string().optional(),
825
+ depth: tool.schema.number().int().optional(),
826
+ limit: tool.schema.number().int().optional(),
827
+ },
828
+ async execute(_args: any, context: any) {
829
+ const commandArgs = [_args.node_id];
830
+ if (_args.relation) commandArgs.push(`--relation=${_args.relation}`);
831
+ if (Number.isInteger(_args.depth)) commandArgs.push(`--depth=${String(_args.depth)}`);
832
+ if (Number.isInteger(_args.limit)) commandArgs.push(`-n`, String(_args.limit));
833
+ return runAgentfCli(context.directory, "memory", "neighbors", commandArgs);
834
+ },
835
+ }),
836
+ "agentf-memory-subgraph": tool({
837
+ description: "Build a subgraph from seed ids.",
838
+ args: {
839
+ seed_ids: tool.schema.array(tool.schema.string()),
840
+ relation_filters: tool.schema.array(tool.schema.string()).optional(),
841
+ depth: tool.schema.number().int().optional(),
842
+ limit: tool.schema.number().int().optional(),
843
+ },
844
+ async execute(_args: any, context: any) {
845
+ const seeds = (_args.seed_ids || []).join(",");
846
+ const commandArgs = [seeds];
847
+ if (_args.relation_filters?.length) commandArgs.push(`--relation=${_args.relation_filters.join(",")}`);
848
+ if (Number.isInteger(_args.depth)) commandArgs.push(`--depth=${String(_args.depth)}`);
849
+ if (Number.isInteger(_args.limit)) commandArgs.push(`-n`, String(_args.limit));
850
+ return runAgentfCli(context.directory, "memory", "subgraph", commandArgs);
851
+ },
852
+ }),
853
+ "agentf-memory-add-lesson": tool({
854
+ description: "Store a lesson memory in Redis.",
855
+ args: {
856
+ title: tool.schema.string(),
857
+ description: tool.schema.string(),
858
+ agent: tool.schema.string().optional(),
859
+ tags: tool.schema.array(tool.schema.string()).optional(),
860
+ context: tool.schema.string().optional(),
861
+ },
862
+ async execute(_args: any, context: any) {
863
+ const commandArgs = [_args.title, _args.description];
864
+ if (_args.agent) commandArgs.push(`--agent=${_args.agent}`);
865
+ if (_args.tags?.length) commandArgs.push(`--tags=${_args.tags.join(",")}`);
866
+ if (_args.context) commandArgs.push(`--context=${_args.context}`);
867
+
868
+ return runAgentfCli(context.directory, "memory", "add-lesson", commandArgs);
869
+ },
870
+ }),
871
+ "agentf-memory-add-success": tool({
872
+ description: "Store a success memory in Redis.",
873
+ args: {
874
+ title: tool.schema.string(),
875
+ description: tool.schema.string(),
876
+ agent: tool.schema.string().optional(),
877
+ tags: tool.schema.array(tool.schema.string()).optional(),
878
+ context: tool.schema.string().optional(),
879
+ },
880
+ async execute(_args: any, context: any) {
881
+ const commandArgs = [_args.title, _args.description];
882
+ if (_args.agent) commandArgs.push(`--agent=${_args.agent}`);
883
+ if (_args.tags?.length) commandArgs.push(`--tags=${_args.tags.join(",")}`);
884
+ if (_args.context) commandArgs.push(`--context=${_args.context}`);
885
+
886
+ return runAgentfCli(context.directory, "memory", "add-success", commandArgs);
887
+ },
888
+ }),
889
+ "agentf-memory-add-pitfall": tool({
890
+ description: "Store a pitfall memory in Redis.",
891
+ args: {
892
+ title: tool.schema.string(),
893
+ description: tool.schema.string(),
894
+ agent: tool.schema.string().optional(),
895
+ tags: tool.schema.array(tool.schema.string()).optional(),
896
+ context: tool.schema.string().optional(),
897
+ },
898
+ async execute(_args: any, context: any) {
899
+ const commandArgs = [_args.title, _args.description];
900
+ if (_args.agent) commandArgs.push(`--agent=${_args.agent}`);
901
+ if (_args.tags?.length) commandArgs.push(`--tags=${_args.tags.join(",")}`);
902
+ if (_args.context) commandArgs.push(`--context=${_args.context}`);
903
+
904
+ return runAgentfCli(context.directory, "memory", "add-pitfall", commandArgs);
905
+ },
906
+ }),
907
+ };
575
908
 
576
- return runAgentfCli(context.directory, "code", "glob", [args.pattern, ...commandArgs]);
577
- },
578
- }),
579
- "agentf-code-grep": tool({
580
- description: "Search file contents via Agentf code CLI.",
581
- args: {
582
- pattern: tool.schema.string().describe("Regex/text to search"),
583
- filePattern: tool.schema.string().optional().describe("Optional include pattern"),
584
- context: tool.schema.number().int().min(0).max(20).optional().describe("Context lines"),
585
- },
586
- async execute(args, context) {
587
- const commandArgs = [];
588
- if (args.filePattern) commandArgs.push(`--file-pattern=${args.filePattern}`);
589
- if (Number.isInteger(args.context)) commandArgs.push(`--context=${args.context}`);
909
+ const agentTools: Record<string, ReturnType<typeof tool>> = {};
910
+ const absDir = path.join(process.cwd(), ".opencode/agents");
590
911
 
591
- return runAgentfCli(context.directory, "code", "grep", [args.pattern, ...commandArgs]);
592
- },
593
- }),
594
- "agentf-code-tree": tool({
595
- description: "Get directory tree data via Agentf code CLI.",
596
- args: {
597
- depth: tool.schema.number().int().min(1).max(10).optional().describe("Max traversal depth"),
598
- },
599
- async execute(args, context) {
600
- const depth = args.depth ?? 3;
601
- return runAgentfCli(context.directory, "code", "tree", [`--depth=${depth}`]);
602
- },
603
- }),
604
- "agentf-code-related-files": tool({
605
- description: "Find import and related file hints for a target file.",
606
- args: {
607
- targetFile: tool.schema.string().describe("Workspace-relative file path"),
608
- },
609
- async execute(args, context) {
610
- return runAgentfCli(context.directory, "code", "related", [args.targetFile]);
611
- },
612
- }),
613
- "agentf-memory-recent": tool({
614
- description: "Get recent Agentf memories from Redis.",
615
- args: {
616
- limit: tool.schema.number().int().min(1).max(100).optional().describe("How many memories to return"),
617
- },
618
- async execute(args, context) {
619
- const limit = args.limit ?? 10;
620
- return runAgentfCli(context.directory, "memory", "recent", ["-n", String(limit)]);
621
- },
622
- }),
623
- "agentf-memory-search": tool({
624
- description: "Search Agentf memories by keyword.",
625
- args: {
626
- query: tool.schema.string().describe("Search query"),
627
- limit: tool.schema.number().int().min(1).max(100).optional().describe("How many results to return"),
628
- },
629
- async execute(args, context) {
630
- const limit = args.limit ?? 10;
631
- return runAgentfCli(context.directory, "memory", "search", [args.query, "-n", String(limit)]);
632
- },
633
- }),
634
- "agentf-memory-by-tag": tool({
635
- description: "Get Agentf memories by tag.",
636
- args: {
637
- tag: tool.schema.string().describe("Tag to filter by"),
638
- limit: tool.schema.number().int().min(1).max(100).optional().describe("How many results to return"),
639
- },
640
- async execute(args, context) {
641
- const limit = args.limit ?? 10;
642
- return runAgentfCli(context.directory, "memory", "by-tag", [args.tag, "-n", String(limit)]);
643
- },
644
- }),
645
- "agentf-memory-by-agent": tool({
646
- description: "Get Agentf memories by agent.",
647
- args: {
648
- agent: tool.schema.string().describe("Agent name"),
649
- limit: tool.schema.number().int().min(1).max(100).optional().describe("How many results to return"),
650
- },
651
- async execute(args, context) {
652
- const limit = args.limit ?? 10;
653
- return runAgentfCli(context.directory, "memory", "by-agent", [args.agent, "-n", String(limit)]);
654
- },
655
- }),
656
- "agentf-memory-by-type": tool({
657
- description: "Get Agentf memories by type.",
658
- args: {
659
- type: tool.schema.string().describe("Memory type (pitfall|lesson|success|business_intent|feature_intent)"),
660
- limit: tool.schema.number().int().min(1).max(100).optional().describe("How many results to return"),
661
- },
662
- async execute(args, context) {
663
- const limit = args.limit ?? 10;
664
- return runAgentfCli(context.directory, "memory", "by-type", [args.type, "-n", String(limit)]);
665
- },
666
- }),
667
- "agentf-memory-tags": tool({
668
- description: "List all unique memory tags.",
669
- args: {},
670
- async execute(_args, context) {
671
- return runAgentfCli(context.directory, "memory", "tags", []);
672
- },
673
- }),
674
- "agentf-memory-pitfalls": tool({
675
- description: "List pitfall memories.",
676
- args: { limit: tool.schema.number().int().min(1).max(100).optional() },
677
- async execute(args, context) {
678
- const limit = args.limit ?? 10;
679
- return runAgentfCli(context.directory, "memory", "pitfalls", ["-n", String(limit)]);
680
- },
681
- }),
682
- "agentf-memory-lessons": tool({
683
- description: "List lesson memories.",
684
- args: { limit: tool.schema.number().int().min(1).max(100).optional() },
685
- async execute(args, context) {
686
- const limit = args.limit ?? 10;
687
- return runAgentfCli(context.directory, "memory", "lessons", ["-n", String(limit)]);
688
- },
689
- }),
690
- "agentf-memory-successes": tool({
691
- description: "List success memories.",
692
- args: { limit: tool.schema.number().int().min(1).max(100).optional() },
693
- async execute(args, context) {
694
- const limit = args.limit ?? 10;
695
- return runAgentfCli(context.directory, "memory", "successes", ["-n", String(limit)]);
696
- },
697
- }),
698
- "agentf-memory-intents": tool({
699
- description: "List intents (business, feature or both).",
700
- args: { kind: tool.schema.string().optional(), limit: tool.schema.number().int().min(1).max(100).optional() },
701
- async execute(args, context) {
702
- const limit = args.limit ?? 10;
703
- const kind = args.kind ? String(args.kind) : "";
704
- const cmdArgs = kind ? [kind, "-n", String(limit)] : ["-n", String(limit)];
705
- return runAgentfCli(context.directory, "memory", "intents", cmdArgs);
706
- },
707
- }),
708
- "agentf-memory-business-intents": tool({
709
- description: "List business intents.",
710
- args: { limit: tool.schema.number().int().min(1).max(100).optional() },
711
- async execute(args, context) {
712
- const limit = args.limit ?? 10;
713
- return runAgentfCli(context.directory, "memory", "business-intents", ["-n", String(limit)]);
714
- },
715
- }),
716
- "agentf-memory-feature-intents": tool({
717
- description: "List feature intents.",
718
- args: { limit: tool.schema.number().int().min(1).max(100).optional() },
719
- async execute(args, context) {
720
- const limit = args.limit ?? 10;
721
- return runAgentfCli(context.directory, "memory", "feature-intents", ["-n", String(limit)]);
722
- },
723
- }),
724
- "agentf-memory-add-business-intent": tool({
725
- description: "Store a business intent in Redis.",
726
- args: {
727
- title: tool.schema.string(),
728
- description: tool.schema.string(),
729
- tags: tool.schema.array(tool.schema.string()).optional(),
730
- constraints: tool.schema.array(tool.schema.string()).optional(),
731
- priority: tool.schema.number().int().optional(),
732
- },
733
- async execute(args, context) {
734
- const commandArgs = [args.title, args.description];
735
- if (args.tags?.length) commandArgs.push(`--tags=${args.tags.join(",")}`);
736
- if (args.constraints?.length) commandArgs.push(`--constraints=${args.constraints.join(";")}`);
737
- if (Number.isInteger(args.priority)) commandArgs.push(`--priority=${String(args.priority)}`);
738
- return runAgentfCli(context.directory, "memory", "add-business-intent", commandArgs);
739
- },
740
- }),
741
- "agentf-memory-add-feature-intent": tool({
742
- description: "Store a feature intent in Redis.",
743
- args: {
744
- title: tool.schema.string(),
745
- description: tool.schema.string(),
746
- tags: tool.schema.array(tool.schema.string()).optional(),
747
- acceptance: tool.schema.array(tool.schema.string()).optional(),
748
- non_goals: tool.schema.array(tool.schema.string()).optional(),
749
- related_task_id: tool.schema.string().optional(),
750
- },
751
- async execute(args, context) {
752
- const commandArgs = [args.title, args.description];
753
- if (args.tags?.length) commandArgs.push(`--tags=${args.tags.join(",")}`);
754
- if (args.acceptance?.length) commandArgs.push(`--acceptance=${args.acceptance.join(";")}`);
755
- if (args.non_goals?.length) commandArgs.push(`--non-goals=${args.non_goals.join(";")}`);
756
- if (args.related_task_id) commandArgs.push(`--task=${args.related_task_id}`);
757
- return runAgentfCli(context.directory, "memory", "add-feature-intent", commandArgs);
758
- },
759
- }),
760
- "agentf-memory-neighbors": tool({
761
- description: "Get neighboring memory nodes by edge traversal.",
762
- args: {
763
- node_id: tool.schema.string(),
764
- relation: tool.schema.string().optional(),
765
- depth: tool.schema.number().int().optional(),
766
- limit: tool.schema.number().int().optional(),
767
- },
768
- async execute(args, context) {
769
- const commandArgs = [args.node_id];
770
- if (args.relation) commandArgs.push(`--relation=${args.relation}`);
771
- if (Number.isInteger(args.depth)) commandArgs.push(`--depth=${String(args.depth)}`);
772
- if (Number.isInteger(args.limit)) commandArgs.push(`-n`, String(args.limit));
773
- return runAgentfCli(context.directory, "memory", "neighbors", commandArgs);
774
- },
775
- }),
776
- "agentf-memory-subgraph": tool({
777
- description: "Build a subgraph from seed ids.",
778
- args: {
779
- seed_ids: tool.schema.array(tool.schema.string()),
780
- relation_filters: tool.schema.array(tool.schema.string()).optional(),
781
- depth: tool.schema.number().int().optional(),
782
- limit: tool.schema.number().int().optional(),
783
- },
784
- async execute(args, context) {
785
- const seeds = (args.seed_ids || []).join(",");
786
- const commandArgs = [seeds];
787
- if (args.relation_filters?.length) commandArgs.push(`--relation=${args.relation_filters.join(",")}`);
788
- if (Number.isInteger(args.depth)) commandArgs.push(`--depth=${String(args.depth)}`);
789
- if (Number.isInteger(args.limit)) commandArgs.push(`-n`, String(args.limit));
790
- return runAgentfCli(context.directory, "memory", "subgraph", commandArgs);
791
- },
792
- }),
793
- "agentf-memory-add-lesson": tool({
794
- description: "Store a lesson memory in Redis.",
795
- args: {
796
- title: tool.schema.string(),
797
- description: tool.schema.string(),
798
- agent: tool.schema.string().optional(),
799
- tags: tool.schema.array(tool.schema.string()).optional(),
800
- context: tool.schema.string().optional(),
801
- },
802
- async execute(args, context) {
803
- const commandArgs = [args.title, args.description];
804
- if (args.agent) commandArgs.push(`--agent=${args.agent}`);
805
- if (args.tags?.length) commandArgs.push(`--tags=${args.tags.join(",")}`);
806
- if (args.context) commandArgs.push(`--context=${args.context}`);
912
+ // Guard: agents directory may not exist in minimal workspaces (eg. tests).
913
+ if (fs.existsSync(absDir)) {
914
+ for (const file of fs.readdirSync(absDir)) {
915
+ const full = path.join(absDir, file);
916
+ if (!fs.statSync(full).isFile()) continue;
917
+ const content = fs.readFileSync(full, "utf8");
918
+ const fm = parseFrontmatter(content);
919
+ const toolName = fm["name"] || path.basename(file, path.extname(file));
807
920
 
808
- return runAgentfCli(context.directory, "memory", "add-lesson", commandArgs);
809
- },
810
- }),
811
- "agentf-memory-add-success": tool({
812
- description: "Store a success memory in Redis.",
813
- args: {
814
- title: tool.schema.string(),
815
- description: tool.schema.string(),
816
- agent: tool.schema.string().optional(),
817
- tags: tool.schema.array(tool.schema.string()).optional(),
818
- context: tool.schema.string().optional(),
819
- },
820
- async execute(args, context) {
821
- const commandArgs = [args.title, args.description];
822
- if (args.agent) commandArgs.push(`--agent=${args.agent}`);
823
- if (args.tags?.length) commandArgs.push(`--tags=${args.tags.join(",")}`);
824
- if (args.context) commandArgs.push(`--context=${args.context}`);
921
+ if ((staticTools as any)[toolName]) continue;
825
922
 
826
- return runAgentfCli(context.directory, "memory", "add-success", commandArgs);
827
- },
828
- }),
829
- "agentf-memory-add-pitfall": tool({
830
- description: "Store a pitfall memory in Redis.",
923
+ const agentName = toolName.replace(/^agentf-/, "");
924
+
925
+ agentTools[toolName] = tool({
926
+ description: `Invoke agent ${agentName} via the agentf CLI. If the result includes confirmation_required=true, ask the user before retrying with confirmedWrite=confirmed.`,
831
927
  args: {
832
- title: tool.schema.string(),
833
- description: tool.schema.string(),
834
- agent: tool.schema.string().optional(),
835
- tags: tool.schema.array(tool.schema.string()).optional(),
836
- context: tool.schema.string().optional(),
928
+ input: tool.schema.string().optional().describe("Optional input prompt or payload"),
929
+ confirmedWrite: tool.schema.string().optional().describe("Continuation token for confirmed writes"),
837
930
  },
838
- async execute(args, context) {
839
- const commandArgs = [args.title, args.description];
840
- if (args.agent) commandArgs.push(`--agent=${args.agent}`);
841
- if (args.tags?.length) commandArgs.push(`--tags=${args.tags.join(",")}`);
842
- if (args.context) commandArgs.push(`--context=${args.context}`);
843
-
844
- return runAgentfCli(context.directory, "memory", "add-pitfall", commandArgs);
931
+ async execute(_args: any, context: any) {
932
+ const cmdArgs: string[] = [];
933
+ // Ensure complex payloads are passed as a single JSON argument so the
934
+ // Ruby CLI can parse structured tasks. Accept strings as-is but
935
+ // stringify objects to avoid `[object Object]` being sent.
936
+ if (_args.input !== undefined) {
937
+ if (typeof _args.input === "object") {
938
+ cmdArgs.push(JSON.stringify(_args.input));
939
+ } else {
940
+ cmdArgs.push(String(_args.input));
941
+ }
942
+ }
943
+ if (_args.confirmedWrite) cmdArgs.push(`--confirmed-write=${_args.confirmedWrite}`);
944
+ return runAgentfCli(context.directory, "agent", agentName, cmdArgs);
845
945
  },
846
- }),
847
- },
848
- };
946
+ });
947
+ }
948
+ }
949
+
950
+ const tools = { ...staticTools, ...agentTools };
951
+
952
+ // The plugin host expects a `tool` map (singular key) in the returned hooks.
953
+ return { tool: tools };
849
954
  };
850
955
 
851
956
  export default agentfPlugin;
852
957
  TYPESCRIPT
853
958
  end
854
959
 
855
- def render_opencode_json
856
- <<~JSON
857
- {
858
- "$schema": "https://opencode.ai/config.json",
859
- "plugin": ["./.opencode/plugins/agentf-plugin"]
960
+ def render_opencode_json(root)
961
+ JSON.pretty_generate(opencode_json_config(root))
962
+ end
963
+
964
+ def opencode_json_config(root)
965
+ base = {
966
+ "$schema" => "https://opencode.ai/config.json"
967
+ }
968
+
969
+ if opencode_plugin_runtime?
970
+ base["plugin"] = ["./.opencode/plugins/agentf-plugin"]
971
+ else
972
+ base["mcp"] = {
973
+ "agentf" => {
974
+ "type" => "local",
975
+ "enabled" => true,
976
+ "command" => opencode_mcp_command(root)
977
+ }
860
978
  }
861
- JSON
979
+ end
980
+
981
+ base
982
+ end
983
+
984
+ def opencode_mcp_command(root)
985
+ [File.join(root, "bin", "agentf"), "mcp-server"]
862
986
  end
863
987
 
864
988
  def write_opencode_json(root)
865
989
  path = File.join(root, "opencode.json")
866
- new_content = JSON.parse(render_opencode_json)
990
+ new_content = JSON.parse(render_opencode_json(root))
867
991
 
868
992
  return write_manifest(path, JSON.pretty_generate(new_content)) unless File.exist?(path)
869
993
 
@@ -875,8 +999,22 @@ module Agentf
875
999
  end
876
1000
 
877
1001
  merged = existing.dup
878
- merged_plugins = Array(existing["plugin"]) + Array(new_content["plugin"])
879
- merged["plugin"] = merged_plugins.uniq
1002
+
1003
+ if new_content["plugin"]
1004
+ merged_plugins = Array(existing["plugin"]) + Array(new_content["plugin"])
1005
+ merged["plugin"] = merged_plugins.uniq
1006
+ elsif existing["plugin"]
1007
+ filtered_plugins = Array(existing["plugin"]).reject { |entry| entry == "./.opencode/plugins/agentf-plugin" }
1008
+ if filtered_plugins.empty?
1009
+ merged.delete("plugin")
1010
+ else
1011
+ merged["plugin"] = filtered_plugins
1012
+ end
1013
+ end
1014
+
1015
+ if new_content["mcp"]
1016
+ merged["mcp"] = (existing["mcp"] || {}).merge(new_content["mcp"])
1017
+ end
880
1018
 
881
1019
  write_manifest(path, JSON.pretty_generate(merged))
882
1020
  end