@uniqbit/mate-opencode-plugin 0.17.0-canary.8 → 0.17.1
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 +25 -8
- package/server.ts +5 -0
- package/src/add-dir.ts +97 -48
- package/src/companion.ts +117 -96
- package/src/server.ts +19 -48
- package/src/tui.tsx +1 -1
- package/tui.tsx +2 -0
package/package.json
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniqbit/mate-opencode-plugin",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.1",
|
|
4
4
|
"description": "OpenCode plugin integration for Mate.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"files": [
|
|
7
|
+
"server.ts",
|
|
8
|
+
"tui.tsx",
|
|
7
9
|
"src/",
|
|
8
10
|
"!src/**/*.test.ts"
|
|
9
11
|
],
|
|
@@ -24,15 +26,30 @@
|
|
|
24
26
|
"format:check": "oxfmt src/ --check"
|
|
25
27
|
},
|
|
26
28
|
"dependencies": {
|
|
27
|
-
"@opencode
|
|
28
|
-
"@
|
|
29
|
-
"@opentui/keymap": "^0.4.5",
|
|
30
|
-
"@opentui/solid": "^0.4.5",
|
|
31
|
-
"@uniqbit/mate-core": "0.17.0-canary.8"
|
|
29
|
+
"@opencode/plugin": "^2.0.23",
|
|
30
|
+
"@uniqbit/mate-core": "0.17.1"
|
|
32
31
|
},
|
|
33
32
|
"devDependencies": {
|
|
34
|
-
"@
|
|
35
|
-
"
|
|
33
|
+
"@opentui/core": "^0.5.14",
|
|
34
|
+
"@opentui/keymap": "^0.5.14",
|
|
35
|
+
"@opentui/solid": "^0.5.14",
|
|
36
|
+
"@types/bun": "^1.3.14"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"@opentui/core": ">=0.5.14",
|
|
40
|
+
"@opentui/keymap": ">=0.5.14",
|
|
41
|
+
"@opentui/solid": ">=0.5.14"
|
|
42
|
+
},
|
|
43
|
+
"peerDependenciesMeta": {
|
|
44
|
+
"@opentui/core": {
|
|
45
|
+
"optional": true
|
|
46
|
+
},
|
|
47
|
+
"@opentui/keymap": {
|
|
48
|
+
"optional": true
|
|
49
|
+
},
|
|
50
|
+
"@opentui/solid": {
|
|
51
|
+
"optional": true
|
|
52
|
+
}
|
|
36
53
|
},
|
|
37
54
|
"engines": {
|
|
38
55
|
"node": ">=24"
|
package/server.ts
ADDED
package/src/add-dir.ts
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
|
|
3
|
-
import type {
|
|
4
|
-
import { readCompanionRuntimeContext } from "@uniqbit/mate-core/runtime";
|
|
4
|
+
import type { Context } from "@opencode/plugin/promise/plugin";
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
return [companionPath, `${companionPath}/**`];
|
|
14
|
-
}
|
|
6
|
+
type CompanionSkill = {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
path: string;
|
|
11
|
+
content: string;
|
|
12
|
+
};
|
|
15
13
|
|
|
16
14
|
function companionSkillPaths(companionPath: string): string[] {
|
|
17
15
|
return [
|
|
@@ -20,50 +18,101 @@ function companionSkillPaths(companionPath: string): string[] {
|
|
|
20
18
|
];
|
|
21
19
|
}
|
|
22
20
|
|
|
23
|
-
function
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
return permission.external_directory as ExternalDirectoryRules;
|
|
21
|
+
function isWithin(root: string, candidate: string): boolean {
|
|
22
|
+
const relative = path.relative(root, path.resolve(candidate));
|
|
23
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
27
24
|
}
|
|
28
25
|
|
|
29
|
-
function
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
26
|
+
function parseFrontmatter(content: string): { name?: string; description?: string } {
|
|
27
|
+
const match = content.match(/^---\s*\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
|
|
28
|
+
if (!match) return {};
|
|
29
|
+
|
|
30
|
+
const scalar = (key: string) => {
|
|
31
|
+
const value = match[1].match(new RegExp(`^${key}:\\s*(.+)$`, "m"))?.[1]?.trim();
|
|
32
|
+
return value?.replace(
|
|
33
|
+
/^(?:"([\s\S]*)"|'([\s\S]*)')$/,
|
|
34
|
+
(_all, doubleQuoted, singleQuoted) => doubleQuoted ?? singleQuoted,
|
|
35
|
+
);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return { name: scalar("name"), description: scalar("description") };
|
|
34
39
|
}
|
|
35
40
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
async function findSkillFiles(directory: string): Promise<string[]> {
|
|
42
|
+
try {
|
|
43
|
+
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
44
|
+
const files = entries
|
|
45
|
+
.filter((entry) => entry.isFile() && entry.name === "SKILL.md")
|
|
46
|
+
.map((entry) => path.join(directory, entry.name));
|
|
47
|
+
const nested = await Promise.all(
|
|
48
|
+
entries
|
|
49
|
+
.filter((entry) => entry.isDirectory())
|
|
50
|
+
.map((entry) => findSkillFiles(path.join(directory, entry.name))),
|
|
51
|
+
);
|
|
52
|
+
return [...files, ...nested.flat()];
|
|
53
|
+
} catch {
|
|
54
|
+
return [];
|
|
45
55
|
}
|
|
56
|
+
}
|
|
46
57
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
58
|
+
function skillID(name: string): string {
|
|
59
|
+
return (
|
|
60
|
+
name
|
|
61
|
+
.toLowerCase()
|
|
62
|
+
.replace(/[^a-z0-9_-]+/g, "-")
|
|
63
|
+
.replace(/^-+|-+$/g, "") || "mate-skill"
|
|
64
|
+
);
|
|
65
|
+
}
|
|
52
66
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
67
|
+
async function loadCompanionSkills(companionPath: string): Promise<CompanionSkill[]> {
|
|
68
|
+
const files = (await Promise.all(companionSkillPaths(companionPath).map(findSkillFiles))).flat();
|
|
69
|
+
const skills = await Promise.all(
|
|
70
|
+
files.map(async (filePath): Promise<CompanionSkill | null> => {
|
|
71
|
+
try {
|
|
72
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
73
|
+
const metadata = parseFrontmatter(content);
|
|
74
|
+
const name = metadata.name || path.basename(path.dirname(filePath));
|
|
75
|
+
return {
|
|
76
|
+
id: skillID(name),
|
|
77
|
+
name,
|
|
78
|
+
...(metadata.description ? { description: metadata.description } : {}),
|
|
79
|
+
path: filePath,
|
|
80
|
+
content,
|
|
81
|
+
};
|
|
82
|
+
} catch {
|
|
83
|
+
/** An unreadable companion skill must not prevent the session from loading. */
|
|
84
|
+
return null;
|
|
57
85
|
}
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
return skills.filter((skill) => skill !== null);
|
|
89
|
+
}
|
|
58
90
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Grants the session access to the companion and registers its skills. The
|
|
93
|
+
* permission hook only widens access for companion resources; every other
|
|
94
|
+
* evaluation keeps OpenCode's own verdict.
|
|
95
|
+
*/
|
|
96
|
+
export async function registerCompanionAccess(api: Context, companionPath: string): Promise<void> {
|
|
97
|
+
const permissionRoot = path.resolve(companionPath);
|
|
98
|
+
|
|
99
|
+
await api.permission.hook("evaluate", (event) => {
|
|
100
|
+
if (event.resources.some((resource) => isWithin(permissionRoot, resource))) {
|
|
101
|
+
event.effect = "allow";
|
|
102
|
+
delete event.message;
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const skills = await loadCompanionSkills(permissionRoot);
|
|
107
|
+
if (skills.length === 0) return;
|
|
68
108
|
|
|
69
|
-
|
|
109
|
+
await api.skill.transform((editor) => {
|
|
110
|
+
for (const skill of skills) {
|
|
111
|
+
if (editor.get(skill.id)) {
|
|
112
|
+
editor.update(skill.id, (current) => Object.assign(current, skill));
|
|
113
|
+
} else {
|
|
114
|
+
editor.add(skill as never);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
package/src/companion.ts
CHANGED
|
@@ -1,24 +1,23 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
|
-
import type {
|
|
4
|
-
import { tool } from "@opencode-ai/plugin";
|
|
3
|
+
import type { Context } from "@opencode/plugin/promise/plugin";
|
|
5
4
|
import {
|
|
6
5
|
COMPANION_POLICY_MARKER,
|
|
7
6
|
MATE_ENV,
|
|
8
7
|
type MateGuidanceFile,
|
|
9
8
|
} from "@uniqbit/mate-core/runtime";
|
|
10
9
|
|
|
11
|
-
import {
|
|
12
|
-
readContext,
|
|
13
|
-
resolveOpenCodeGuidance,
|
|
14
|
-
type CompanionContext,
|
|
15
|
-
} from "@uniqbit/mate-core/opencode";
|
|
10
|
+
import { resolveOpenCodeGuidance, type CompanionContext } from "@uniqbit/mate-core/opencode";
|
|
16
11
|
|
|
17
12
|
function prependPathEntry(pathValue: string | undefined, entry: string): string {
|
|
18
13
|
const entries = (pathValue ?? "").split(path.delimiter).filter(Boolean);
|
|
19
14
|
return [entry, ...entries.filter((value) => value !== entry)].join(path.delimiter);
|
|
20
15
|
}
|
|
21
16
|
|
|
17
|
+
function resolveWrapperBinPath(): string {
|
|
18
|
+
return process.env.MATE_WRAPPER_BIN_PATH ?? "$MATE_WRAPPER_BIN_PATH";
|
|
19
|
+
}
|
|
20
|
+
|
|
22
21
|
function buildStartupError(details: string[]): Error {
|
|
23
22
|
return new Error(
|
|
24
23
|
[
|
|
@@ -34,14 +33,14 @@ function buildStartupError(details: string[]): Error {
|
|
|
34
33
|
* an Unmanaged Session in a wrapped repository builds the same payload from the
|
|
35
34
|
* companion the projection names, with the Capability names read live.
|
|
36
35
|
*/
|
|
37
|
-
function loadGuidance(): MateGuidanceFile | null {
|
|
38
|
-
const { guidance, errors } = resolveOpenCodeGuidance();
|
|
36
|
+
function loadGuidance(directory: string): MateGuidanceFile | null {
|
|
37
|
+
const { guidance, errors } = resolveOpenCodeGuidance(process.env, directory);
|
|
39
38
|
if (errors.length > 0) throw buildStartupError(errors);
|
|
40
39
|
return guidance;
|
|
41
40
|
}
|
|
42
41
|
|
|
43
42
|
function materializeCompanionGuidance(guidance: string, context: CompanionContext): string {
|
|
44
|
-
const wrapperBinPath =
|
|
43
|
+
const wrapperBinPath = resolveWrapperBinPath();
|
|
45
44
|
return guidance
|
|
46
45
|
.replaceAll("$MATE_REPO_PATH", context.repositoryPath)
|
|
47
46
|
.replaceAll("$MATE_ARTIFACT_PATH", context.companionPath)
|
|
@@ -63,95 +62,117 @@ function buildSystemPrompt(context: CompanionContext, guidance: MateGuidanceFile
|
|
|
63
62
|
return lines;
|
|
64
63
|
}
|
|
65
64
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (!
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const guidance = loadGuidance();
|
|
73
|
-
if (!guidance) return {};
|
|
65
|
+
function readSystemText(part: unknown): string {
|
|
66
|
+
if (typeof part === "string") return part;
|
|
67
|
+
if (typeof part !== "object" || part === null || !("text" in part)) return "";
|
|
68
|
+
return typeof part.text === "string" ? part.text : "";
|
|
69
|
+
}
|
|
74
70
|
|
|
75
|
-
|
|
71
|
+
/** Session-scoped variables a spawned shell sees; mutates `env` in place. */
|
|
72
|
+
function applyCompanionShellEnv(
|
|
73
|
+
context: CompanionContext,
|
|
74
|
+
env: Record<string, string | undefined>,
|
|
75
|
+
): void {
|
|
76
|
+
const wrapperBinPath = resolveWrapperBinPath();
|
|
77
|
+
env.MATE_NAME = context.frameworkName;
|
|
78
|
+
env.MATE_VERSION = process.env.MATE_VERSION ?? "unknown";
|
|
79
|
+
env.MATE_ARTIFACT_PATH = context.companionPath;
|
|
80
|
+
env.MATE_WRAPPER_BIN_PATH = wrapperBinPath;
|
|
81
|
+
if (context.repositoryPath) {
|
|
82
|
+
env.MATE_REPO_PATH = context.repositoryPath;
|
|
83
|
+
env.MATE_REPO_ID = context.repositoryId;
|
|
84
|
+
} else {
|
|
85
|
+
delete env.MATE_REPO_PATH;
|
|
86
|
+
delete env.MATE_REPO_ID;
|
|
87
|
+
}
|
|
88
|
+
env.MATE_POLICY_JSON = context.policyJson;
|
|
89
|
+
env.MATE_GRAPHIFY_ENABLED = context.graphifyEnabled ? "1" : "0";
|
|
90
|
+
env.MATE_GIT_AUTO_MODE = context.gitAutoModeEnabled ? "1" : "0";
|
|
91
|
+
/**
|
|
92
|
+
* Session-scoped payload consumed at plugin startup: masked so the multi-KB
|
|
93
|
+
* guidance JSON never leaks into spawned shells (and a nested `mate` launch
|
|
94
|
+
* can never inherit a stale copy).
|
|
95
|
+
*/
|
|
96
|
+
env[MATE_ENV.guidanceJson] = "";
|
|
97
|
+
env.PATH = prependPathEntry(process.env.PATH, wrapperBinPath);
|
|
98
|
+
}
|
|
76
99
|
|
|
100
|
+
function companionPathsResult(context: CompanionContext) {
|
|
101
|
+
const wrapperBinPath = resolveWrapperBinPath();
|
|
77
102
|
return {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
2,
|
|
95
|
-
),
|
|
96
|
-
metadata: {
|
|
97
|
-
companionPath: context.companionPath,
|
|
98
|
-
wrapperBinPath,
|
|
99
|
-
...(context.repositoryPath ? { repositoryPath: context.repositoryPath } : {}),
|
|
100
|
-
},
|
|
101
|
-
};
|
|
102
|
-
},
|
|
103
|
-
}),
|
|
104
|
-
},
|
|
105
|
-
"experimental.chat.system.transform": async (_input: any, output: { system: string[] }) => {
|
|
106
|
-
// A wrapped repository lists this plugin in its own project config, so a
|
|
107
|
-
// managed launch — whose OPENCODE_CONFIG_DIR names the companion, which
|
|
108
|
-
// lists it too — loads it twice. The marker is what makes the second load
|
|
109
|
-
// inert instead of appending the companion block a second time.
|
|
110
|
-
if (output.system.some((part) => part.includes(COMPANION_POLICY_MARKER))) return;
|
|
111
|
-
|
|
112
|
-
// Collapse the whole system prompt into a single entry. opencode expands
|
|
113
|
-
// each `system[]` element into its own `role:"system"` wire message, and
|
|
114
|
-
// some self-hosted chat templates (e.g. Qwen served via vLLM) reject any
|
|
115
|
-
// system message that is not the very first message. Merging opencode's
|
|
116
|
-
// own parts with the mate companion block guarantees exactly one leading
|
|
117
|
-
// system message.
|
|
118
|
-
const companion = buildSystemPrompt(context, guidance).join("\n");
|
|
119
|
-
const merged = [...output.system, companion]
|
|
120
|
-
.map((part) => part.trim())
|
|
121
|
-
.filter((part) => part.length > 0)
|
|
122
|
-
.join("\n\n");
|
|
123
|
-
output.system.length = 0;
|
|
124
|
-
if (merged.length > 0) {
|
|
125
|
-
output.system.push(merged);
|
|
126
|
-
}
|
|
127
|
-
},
|
|
128
|
-
"experimental.session.compacting": async (_input: any, output: { context: string[] }) => {
|
|
129
|
-
if (output.context.some((part) => part.includes(COMPANION_POLICY_MARKER))) return;
|
|
130
|
-
output.context.push(buildSystemPrompt(context, guidance).join("\n"));
|
|
131
|
-
},
|
|
132
|
-
"shell.env": async (_input: any, output: { env: Record<string, string> }) => {
|
|
133
|
-
output.env.MATE_NAME = context.frameworkName;
|
|
134
|
-
output.env.MATE_VERSION = process.env.MATE_VERSION ?? "unknown";
|
|
135
|
-
output.env.MATE_ARTIFACT_PATH = context.companionPath;
|
|
136
|
-
output.env.MATE_WRAPPER_BIN_PATH = wrapperBinPath;
|
|
137
|
-
if (context.repositoryPath) {
|
|
138
|
-
output.env.MATE_REPO_PATH = context.repositoryPath;
|
|
139
|
-
output.env.MATE_REPO_ID = context.repositoryId;
|
|
140
|
-
} else {
|
|
141
|
-
delete output.env.MATE_REPO_PATH;
|
|
142
|
-
delete output.env.MATE_REPO_ID;
|
|
143
|
-
}
|
|
144
|
-
output.env.MATE_POLICY_JSON = context.policyJson;
|
|
145
|
-
output.env.MATE_GRAPHIFY_ENABLED = context.graphifyEnabled ? "1" : "0";
|
|
146
|
-
output.env.MATE_GIT_AUTO_MODE = context.gitAutoModeEnabled ? "1" : "0";
|
|
147
|
-
// Session-scoped payload consumed above at plugin startup: mask it so
|
|
148
|
-
// the multi-KB guidance JSON never leaks into spawned shells (and a
|
|
149
|
-
// nested `mate` launch can never inherit a stale copy).
|
|
150
|
-
output.env[MATE_ENV.guidanceJson] = "";
|
|
151
|
-
|
|
152
|
-
output.env.PATH = prependPathEntry(process.env.PATH, wrapperBinPath);
|
|
103
|
+
content: JSON.stringify(
|
|
104
|
+
{
|
|
105
|
+
companionFrameworkPath: context.companionPath,
|
|
106
|
+
wrapperBinPath,
|
|
107
|
+
...(context.repositoryPath
|
|
108
|
+
? { repositoryPath: context.repositoryPath, repositoryId: context.repositoryId }
|
|
109
|
+
: {}),
|
|
110
|
+
policy: JSON.parse(context.policyJson || "{}"),
|
|
111
|
+
},
|
|
112
|
+
null,
|
|
113
|
+
2,
|
|
114
|
+
),
|
|
115
|
+
metadata: {
|
|
116
|
+
companionPath: context.companionPath,
|
|
117
|
+
wrapperBinPath,
|
|
118
|
+
...(context.repositoryPath ? { repositoryPath: context.repositoryPath } : {}),
|
|
153
119
|
},
|
|
154
120
|
};
|
|
155
|
-
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Collapses the whole system prompt into a single entry. OpenCode sends each
|
|
125
|
+
* `system[]` element as its own system message, and some self-hosted chat
|
|
126
|
+
* templates (e.g. Qwen served via vLLM) reject any system message that is not
|
|
127
|
+
* the very first one.
|
|
128
|
+
*/
|
|
129
|
+
function mergeCompanionSystem(system: unknown[], companion: string): string[] {
|
|
130
|
+
const texts = system.map(readSystemText);
|
|
131
|
+
/**
|
|
132
|
+
* A wrapped repository lists this plugin in its own project config, so a
|
|
133
|
+
* managed launch loads it twice; the marker keeps the second load inert.
|
|
134
|
+
*/
|
|
135
|
+
if (texts.some((text) => text.includes(COMPANION_POLICY_MARKER))) return texts;
|
|
136
|
+
const merged = [...texts, companion]
|
|
137
|
+
.map((part) => part.trim())
|
|
138
|
+
.filter((part) => part.length > 0)
|
|
139
|
+
.join("\n\n");
|
|
140
|
+
return merged.length > 0 ? [merged] : [];
|
|
141
|
+
}
|
|
156
142
|
|
|
157
|
-
|
|
143
|
+
/** Registers guidance, compaction context, shell env, and `companion_paths`. */
|
|
144
|
+
export async function registerCompanion(api: Context, context: CompanionContext): Promise<void> {
|
|
145
|
+
const guidance = loadGuidance(api.location.directory);
|
|
146
|
+
if (!guidance) return;
|
|
147
|
+
|
|
148
|
+
const companion = buildSystemPrompt(context, guidance).join("\n");
|
|
149
|
+
|
|
150
|
+
await api.session.hook("context", (event) => {
|
|
151
|
+
const merged = mergeCompanionSystem(event.system, companion);
|
|
152
|
+
event.system.splice(
|
|
153
|
+
0,
|
|
154
|
+
event.system.length,
|
|
155
|
+
...merged.map((text) => ({ type: "text" as const, text }) as never),
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
await api.session.hook("compaction", (event) => {
|
|
160
|
+
if (event.system.some((part) => readSystemText(part).includes(COMPANION_POLICY_MARKER))) return;
|
|
161
|
+
event.system.push({ type: "text", text: companion } as never);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
await api.shell.hook("create.before", (event) => {
|
|
165
|
+
applyCompanionShellEnv(context, event.env);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
await api.tool.transform((editor) => {
|
|
169
|
+
editor.add({
|
|
170
|
+
name: "companion_paths",
|
|
171
|
+
description: `Return active ${context.frameworkName} companion framework and working repository paths.`,
|
|
172
|
+
input: { type: "object", properties: {}, additionalProperties: false },
|
|
173
|
+
async execute() {
|
|
174
|
+
return companionPathsResult(context);
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -1,54 +1,25 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
1
|
+
import { Plugin } from "@opencode/plugin";
|
|
2
|
+
import { readContext, registerCompanionHooks } from "@uniqbit/mate-core/opencode";
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { CompanionPlugin } from "./companion";
|
|
7
|
-
|
|
8
|
-
// Compose the implementation modules in the execution order the previously
|
|
9
|
-
// copied plugin files were discovered in (alphabetical file order:
|
|
10
|
-
// mate-add-dir, mate-companion-hooks, mate-companion).
|
|
11
|
-
const MODULES: Plugin[] = [AddDirPlugin, CompanionHooksPlugin, CompanionPlugin];
|
|
12
|
-
|
|
13
|
-
type HookValue = Hooks[keyof Hooks];
|
|
14
|
-
|
|
15
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
16
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function chainHooks(first: HookValue, second: HookValue): HookValue {
|
|
20
|
-
if (typeof first === "function" && typeof second === "function") {
|
|
21
|
-
return (async (...args: unknown[]) => {
|
|
22
|
-
await (first as (...values: unknown[]) => Promise<unknown>)(...args);
|
|
23
|
-
await (second as (...values: unknown[]) => Promise<unknown>)(...args);
|
|
24
|
-
}) as HookValue;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (isRecord(first) && isRecord(second)) {
|
|
28
|
-
return { ...first, ...second } as HookValue;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return second;
|
|
32
|
-
}
|
|
4
|
+
import { registerCompanionAccess } from "./add-dir";
|
|
5
|
+
import { registerCompanion } from "./companion";
|
|
33
6
|
|
|
34
7
|
/**
|
|
35
|
-
*
|
|
36
|
-
* `./server` export; stays inert when
|
|
37
|
-
*
|
|
8
|
+
* Mate OpenCode server plugin (OpenCode 2.x). Loaded through the package's
|
|
9
|
+
* `./server` export; stays inert when no companion resolves from the launch
|
|
10
|
+
* environment or a Projection Root above the session directory.
|
|
38
11
|
*/
|
|
39
|
-
export const MateOpenCodePlugin
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return merged as Hooks;
|
|
52
|
-
};
|
|
12
|
+
export const MateOpenCodePlugin = Plugin.define({
|
|
13
|
+
id: "mate-opencode-plugin",
|
|
14
|
+
async setup(api) {
|
|
15
|
+
const context = readContext(process.env, api.location.directory);
|
|
16
|
+
if (!context.companionPath) return;
|
|
17
|
+
|
|
18
|
+
await registerCompanionAccess(api, context.companionPath);
|
|
19
|
+
const cleanup = await registerCompanionHooks(api, context);
|
|
20
|
+
await registerCompanion(api, context);
|
|
21
|
+
return cleanup;
|
|
22
|
+
},
|
|
23
|
+
});
|
|
53
24
|
|
|
54
25
|
export default MateOpenCodePlugin;
|
package/src/tui.tsx
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { default } from "@uniqbit/mate-core/opencode/tui";
|
package/tui.tsx
ADDED