diff --git a/packages/core/src/plugin/host.ts b/packages/core/src/plugin/host.ts index ae6ce7727f1..d1130d907bf 100644 --- a/packages/core/src/plugin/host.ts +++ b/packages/core/src/plugin/host.ts @@ -73,6 +73,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: Interface, p return { app, + location: locationInfo(), options: {}, agent: { get: (input) => { diff --git a/packages/core/test/plugin.test.ts b/packages/core/test/plugin.test.ts index eec02920820..7dbbe0aeb79 100644 --- a/packages/core/test/plugin.test.ts +++ b/packages/core/test/plugin.test.ts @@ -25,6 +25,34 @@ class Secret extends Context.Service()("@opencode/test/PluginSec const versioned = (plugin: EffectPlugin.Plugin, version = "1") => ({ ...plugin, version }) describe("Plugin", () => { + it.effect("exposes the current location to activated plugins", () => + Effect.gen(function* () { + const plugins = yield* Plugin.Service + const location = yield* Location.Service + const seen: Location.Info[] = [] + yield* plugins.activate([ + versioned( + EffectPlugin.define({ + id: "location-context", + effect: (ctx) => + Effect.sync(() => { + seen.push(ctx.location) + }), + }), + "1", + ), + ]) + + expect(seen).toEqual([ + new Location.Info({ + directory: location.directory, + workspaceID: location.workspaceID, + project: location.project, + }), + ]) + }), + ) + it.live("exposes public events through the plugin context", () => Effect.gen(function* () { const plugins = yield* Plugin.Service diff --git a/packages/core/test/plugin/host.ts b/packages/core/test/plugin/host.ts index 46c74fd54f6..a20379b601f 100644 --- a/packages/core/test/plugin/host.ts +++ b/packages/core/test/plugin/host.ts @@ -18,6 +18,16 @@ type Overrides = Partial> & { export function host(overrides: Overrides = {}): Plugin.Context { return { app: overrides.app ?? { name: "test", version: "test", channel: "test" }, + location: + overrides.location ?? + new Location.Info({ + directory: AbsolutePath.make("/workspace"), + project: { + id: Project.ID.global, + directory: AbsolutePath.make("/workspace"), + canonical: AbsolutePath.make("/workspace"), + }, + }), options: {}, agent: overrides.agent ?? { get: () => Effect.die("unused agent.get"), diff --git a/packages/core/test/plugin/promise.test.ts b/packages/core/test/plugin/promise.test.ts index 125a15a9710..d107e3a6917 100644 --- a/packages/core/test/plugin/promise.test.ts +++ b/packages/core/test/plugin/promise.test.ts @@ -17,6 +17,7 @@ import { SessionInbox } from "@opencode-ai/core/session/inbox" import { Tool } from "@opencode-ai/core/tool" import { Provider } from "@opencode-ai/core/provider" import { Project } from "@opencode-ai/core/project" +import { Workspace } from "@opencode-ai/core/workspace" import { AbsolutePath } from "@opencode-ai/core/schema" import { define } from "@opencode-ai/plugin/promise/plugin" import { Money } from "@opencode-ai/schema/money" @@ -28,6 +29,41 @@ import { host as testHost } from "./host" const it = testEffect(PluginTestLayer) describe("fromPromise", () => { + it.effect("exposes the host location including workspace and project metadata", () => + Effect.gen(function* () { + const plugins = yield* Plugin.Service + const location = yield* Location.Service + const expected = new Location.Info({ + directory: AbsolutePath.make("/worktree/packages/app"), + workspaceID: Workspace.ID.make("wrk_plugin_location"), + project: { + id: Project.ID.global, + directory: AbsolutePath.make("/worktree"), + canonical: AbsolutePath.make("/project"), + }, + }) + const host = yield* PluginHost.make(plugins).pipe( + Effect.provideService(Location.Service, { + ...location, + directory: expected.directory, + workspaceID: expected.workspaceID, + project: expected.project, + }), + ) + const seen: Location.Info[] = [] + yield* PluginPromise.fromPromise( + define({ + id: "promise-location", + setup: (ctx) => { + seen.push(ctx.location) + }, + }), + ).effect(host) + + expect(seen).toEqual([expected]) + }), + ) + it.effect("adapts plugin storage methods", () => Effect.gen(function* () { const plugins = yield* Plugin.Service diff --git a/packages/plugin/src/effect/index.ts b/packages/plugin/src/effect/index.ts index adc1b66a95b..fbf4e2e014b 100644 --- a/packages/plugin/src/effect/index.ts +++ b/packages/plugin/src/effect/index.ts @@ -6,6 +6,7 @@ export { Command } from "@opencode-ai/schema/command" export { Connection } from "@opencode-ai/schema/connection" export { Credential } from "@opencode-ai/schema/credential" export { Integration } from "@opencode-ai/schema/integration" +export { Location } from "@opencode-ai/schema/location" export { Mcp } from "@opencode-ai/schema/mcp" export { Model } from "@opencode-ai/schema/model" export { Provider } from "@opencode-ai/schema/provider" diff --git a/packages/plugin/src/effect/plugin.ts b/packages/plugin/src/effect/plugin.ts index 09a7aaa8b06..a8a45143e10 100644 --- a/packages/plugin/src/effect/plugin.ts +++ b/packages/plugin/src/effect/plugin.ts @@ -1,4 +1,5 @@ import type { GenerateApi, PluginApi } from "@opencode-ai/client/effect/api" +import type { Location } from "@opencode-ai/schema/location" import type { Effect, Scope } from "effect" import type { PluginOptions } from "../options.js" import type { VcsDiscovery } from "../vcs.js" @@ -22,6 +23,7 @@ import type { WebSearchDomain } from "./websearch.js" export interface Context { readonly app: App + readonly location: Location.Info readonly options: PluginOptions readonly agent: AgentDomain readonly aisdk: AISDKDomain diff --git a/packages/plugin/src/promise/adapter.ts b/packages/plugin/src/promise/adapter.ts index 02936b6d56b..04342f28a24 100644 --- a/packages/plugin/src/promise/adapter.ts +++ b/packages/plugin/src/promise/adapter.ts @@ -126,6 +126,7 @@ export function fromPromise(plugin: Plugin) { const context2: Context = { app: host.app, + location: host.location, options: host.options, agent: { get: adaptApiMethod(AgentEndpoints["agent.get"], host.agent.get), diff --git a/packages/plugin/src/promise/index.ts b/packages/plugin/src/promise/index.ts index e1a6079050f..9c799b700da 100644 --- a/packages/plugin/src/promise/index.ts +++ b/packages/plugin/src/promise/index.ts @@ -7,6 +7,7 @@ export { Command } from "@opencode-ai/schema/command" export { Connection } from "@opencode-ai/schema/connection" export { Credential } from "@opencode-ai/schema/credential" export { Integration } from "@opencode-ai/schema/integration" +export { Location } from "@opencode-ai/schema/location" export { Mcp } from "@opencode-ai/schema/mcp" export { Model } from "@opencode-ai/schema/model" export { Provider } from "@opencode-ai/schema/provider" diff --git a/packages/plugin/src/promise/plugin.ts b/packages/plugin/src/promise/plugin.ts index c3150efd2ed..27fa5c484a7 100644 --- a/packages/plugin/src/promise/plugin.ts +++ b/packages/plugin/src/promise/plugin.ts @@ -1,4 +1,5 @@ import type { GenerateApi, PluginApi } from "@opencode-ai/client/promise/api" +import type { Location } from "@opencode-ai/schema/location" import type { PluginOptions } from "../options.js" import type { VcsDiscovery } from "../vcs.js" import type { App } from "../app.js" @@ -21,6 +22,7 @@ import type { WebSearchDomain } from "./websearch.js" export interface Context { readonly app: App + readonly location: Location.Info readonly options: PluginOptions readonly agent: AgentDomain readonly aisdk: AISDKDomain diff --git a/packages/plugin/test/contract-identity.test.ts b/packages/plugin/test/contract-identity.test.ts index 2281c64d2d7..54ca5123df2 100644 --- a/packages/plugin/test/contract-identity.test.ts +++ b/packages/plugin/test/contract-identity.test.ts @@ -5,6 +5,7 @@ import { Command } from "@opencode-ai/schema/command" import { Connection } from "@opencode-ai/schema/connection" import { Credential } from "@opencode-ai/schema/credential" import { Integration } from "@opencode-ai/schema/integration" +import { Location } from "@opencode-ai/schema/location" import { Mcp } from "@opencode-ai/schema/mcp" import { Model } from "@opencode-ai/schema/model" import { Provider } from "@opencode-ai/schema/provider" @@ -26,6 +27,7 @@ test.each([ expect(entrypoint.Connection).toBe(Connection) expect(entrypoint.Credential).toBe(Credential) expect(entrypoint.Integration).toBe(Integration) + expect(entrypoint.Location).toBe(Location) expect(entrypoint.Mcp).toBe(Mcp) expect(entrypoint.Model).toBe(Model) expect(entrypoint.Provider).toBe(Provider) @@ -39,6 +41,7 @@ test.each([ "Connection", "Credential", "Integration", + "Location", "Mcp", "Model", "Plugin", diff --git a/packages/www/src/docs/content/build/plugins/index.mdx b/packages/www/src/docs/content/build/plugins/index.mdx index 28de9671f9d..4bfe8457957 100644 --- a/packages/www/src/docs/content/build/plugins/index.mdx +++ b/packages/www/src/docs/content/build/plugins/index.mdx @@ -72,6 +72,19 @@ setup(ctx) { } ``` +`ctx.location` describes the location where this plugin instance is loaded. It +includes `directory`, optional `workspaceID`, and `project` metadata (`id`, +`directory`, and `canonical`). It is available in both Promise and Effect plugins. +This is the plugin instance's location, not the location of every session it can +access or event it receives. + +```ts +setup(ctx) { + console.log(ctx.location.directory) + console.log(ctx.location.project.canonical) +} +``` + ### Options Pass plugin options with the object form in `opencode.json(c)`.