@uniqbit/mate-opencode-plugin 0.15.5-canary.4 → 0.16.0-canary.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniqbit/mate-opencode-plugin",
3
- "version": "0.15.5-canary.4",
3
+ "version": "0.16.0-canary.0",
4
4
  "description": "OpenCode plugin integration for Mate.",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -28,7 +28,7 @@
28
28
  "@opentui/core": "^0.4.5",
29
29
  "@opentui/keymap": "^0.4.5",
30
30
  "@opentui/solid": "^0.4.5",
31
- "@uniqbit/mate-core": "0.15.5-canary.4"
31
+ "@uniqbit/mate-core": "0.16.0-canary.0"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@types/bun": "^1.3.14",
package/src/add-dir.ts CHANGED
@@ -1,24 +1,45 @@
1
1
  import path from "node:path";
2
2
 
3
3
  import type { Config, Hooks, Plugin } from "@opencode-ai/plugin";
4
+ import { readCompanionRuntimeContext } from "@uniqbit/mate-core/runtime";
4
5
 
5
6
  // OpenCode accepts a per-path rule map for external_directory at runtime even
6
7
  // though the published Config type narrows it to a single rule value.
7
8
  type ExternalDirectoryRules = Record<string, "allow" | "ask" | "deny" | undefined>;
8
9
  type ConfigHook = NonNullable<Hooks["config"]>;
10
+ type SkillConfig = { skills?: { paths?: string[] } };
9
11
 
10
12
  function companionPathRules(companionPath: string): string[] {
11
13
  return [companionPath, `${companionPath}/**`];
12
14
  }
13
15
 
16
+ function companionSkillPaths(companionPath: string): string[] {
17
+ return [
18
+ path.join(companionPath, ".agents", "skills"),
19
+ path.join(companionPath, ".opencode", "skills"),
20
+ ];
21
+ }
22
+
14
23
  function externalDirectoryRules(config: Config): ExternalDirectoryRules {
15
24
  const permission = (config.permission ??= {}) as { external_directory?: unknown };
16
25
  permission.external_directory ??= {};
17
26
  return permission.external_directory as ExternalDirectoryRules;
18
27
  }
19
28
 
29
+ function skillPaths(config: Config): string[] {
30
+ const skillConfig = config as Config & SkillConfig;
31
+ skillConfig.skills ??= {};
32
+ skillConfig.skills.paths ??= [];
33
+ return skillConfig.skills.paths;
34
+ }
35
+
36
+ /**
37
+ * Resolves through the composed reader — launch environment first, Projection
38
+ * Root second — so a wrapped Working Repository allow-lists its companion in a
39
+ * session Mate did not start. No variable name is spelled here.
40
+ */
20
41
  export const AddDirPlugin: Plugin = async () => {
21
- const companionPath = process.env.MATE_ARTIFACT_PATH?.trim();
42
+ const companionPath = readCompanionRuntimeContext().companionPath.trim();
22
43
  if (!companionPath) {
23
44
  return {};
24
45
  }
@@ -34,6 +55,13 @@ export const AddDirPlugin: Plugin = async () => {
34
55
  externalDirectory[rule] = "allow";
35
56
  }
36
57
  }
58
+
59
+ const paths = skillPaths(cfg);
60
+ for (const skillPath of companionSkillPaths(resolvedCompanionPath)) {
61
+ if (!paths.includes(skillPath)) {
62
+ paths.push(skillPath);
63
+ }
64
+ }
37
65
  }) satisfies ConfigHook,
38
66
  };
39
67
  };
package/src/companion.ts CHANGED
@@ -2,9 +2,17 @@ import path from "node:path";
2
2
 
3
3
  import type { Plugin } from "@opencode-ai/plugin";
4
4
  import { tool } from "@opencode-ai/plugin";
5
- import { MATE_ENV, parseGuidanceContent, type MateGuidanceFile } from "@uniqbit/mate-core/runtime";
6
-
7
- import { readContext, type CompanionContext } from "@uniqbit/mate-core/opencode";
5
+ import {
6
+ COMPANION_POLICY_MARKER,
7
+ MATE_ENV,
8
+ type MateGuidanceFile,
9
+ } from "@uniqbit/mate-core/runtime";
10
+
11
+ import {
12
+ readContext,
13
+ resolveOpenCodeGuidance,
14
+ type CompanionContext,
15
+ } from "@uniqbit/mate-core/opencode";
8
16
 
9
17
  function prependPathEntry(pathValue: string | undefined, entry: string): string {
10
18
  const entries = (pathValue ?? "").split(path.delimiter).filter(Boolean);
@@ -16,22 +24,19 @@ function buildStartupError(details: string[]): Error {
16
24
  [
17
25
  "Mate OpenCode companion plugin is incomplete and cannot start.",
18
26
  ...details.map((detail) => `- ${detail}`),
19
- "Launch through `mate opencode` so the CLI injects the companion guidance.",
27
+ "Launch through `mate opencode`, or run `mate wrap` so the guidance resolves from the Projection Root.",
20
28
  ].join("\n"),
21
29
  );
22
30
  }
23
31
 
24
- function loadGuidance(): MateGuidanceFile {
25
- const raw = process.env[MATE_ENV.guidanceJson];
26
- if (!raw || !raw.trim()) {
27
- throw buildStartupError([`missing ${MATE_ENV.guidanceJson} in the launch environment`]);
28
- }
29
-
30
- const { guidance, errors } = parseGuidanceContent(raw);
31
- if (errors.length > 0) {
32
- throw buildStartupError(errors);
33
- }
34
-
32
+ /**
33
+ * Env first, projection second. A session Mate launched carries the payload;
34
+ * an Unmanaged Session in a wrapped repository builds the same payload from the
35
+ * companion the projection names, with the Capability names read live.
36
+ */
37
+ function loadGuidance(): MateGuidanceFile | null {
38
+ const { guidance, errors } = resolveOpenCodeGuidance();
39
+ if (errors.length > 0) throw buildStartupError(errors);
35
40
  return guidance;
36
41
  }
37
42
 
@@ -59,12 +64,13 @@ function buildSystemPrompt(context: CompanionContext, guidance: MateGuidanceFile
59
64
  }
60
65
 
61
66
  export const CompanionPlugin: Plugin = async () => {
62
- const context = readContext(process.env.MATE_ARTIFACT_PATH ?? "");
67
+ const context = readContext();
63
68
  if (!context.companionPath || !context.repositoryPath) {
64
69
  return {};
65
70
  }
66
71
 
67
72
  const guidance = loadGuidance();
73
+ if (!guidance) return {};
68
74
 
69
75
  const wrapperBinPath = process.env.MATE_WRAPPER_BIN_PATH ?? "$MATE_WRAPPER_BIN_PATH";
70
76
 
@@ -96,6 +102,12 @@ export const CompanionPlugin: Plugin = async () => {
96
102
  }),
97
103
  },
98
104
  "experimental.chat.system.transform": async (_input: any, output: { system: string[] }) => {
105
+ // A wrapped repository lists this plugin in its own project config, so a
106
+ // managed launch — whose OPENCODE_CONFIG_DIR names the companion, which
107
+ // lists it too — loads it twice. The marker is what makes the second load
108
+ // inert instead of appending the companion block a second time.
109
+ if (output.system.some((part) => part.includes(COMPANION_POLICY_MARKER))) return;
110
+
99
111
  // Collapse the whole system prompt into a single entry. opencode expands
100
112
  // each `system[]` element into its own `role:"system"` wire message, and
101
113
  // some self-hosted chat templates (e.g. Qwen served via vLLM) reject any
@@ -113,6 +125,7 @@ export const CompanionPlugin: Plugin = async () => {
113
125
  }
114
126
  },
115
127
  "experimental.session.compacting": async (_input: any, output: { context: string[] }) => {
128
+ if (output.context.some((part) => part.includes(COMPANION_POLICY_MARKER))) return;
116
129
  output.context.push(buildSystemPrompt(context, guidance).join("\n"));
117
130
  },
118
131
  "shell.env": async (_input: any, output: { env: Record<string, string> }) => {