@uniqbit/mate-core 0.18.4 → 0.18.5
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/opencode-plugin/src/add-dir.ts +76 -10
- package/opencode-plugin/src/companion.ts +1 -8
- package/opencode-plugin/src/server.ts +2 -15
- package/package.json +1 -1
- package/src/cli/commands/artifact/finish/openspec.ts +40 -8
- package/src/cli/commands/doctor.ts +92 -86
- package/src/cli/commands/install.ts +0 -8
- package/src/cli/commands/studio/terminal-launch.ts +7 -16
- package/src/cli/commands/wrap.ts +4 -29
- package/src/cli/usage.ts +1 -2
- package/src/hooks/session-guidance.ts +2 -20
- package/src/lib/orchestrator/adapters/claude.ts +20 -130
- package/src/lib/orchestrator/adapters/opencode.ts +7 -24
- package/src/lib/orchestrator/config-store.ts +2 -70
- package/src/lib/orchestrator/projection-claude-skills.ts +35 -88
- package/src/lib/orchestrator/projection-entries.ts +0 -1
- package/src/lib/orchestrator/projection-types.ts +0 -2
- package/src/lib/orchestrator/session-prepare.ts +4 -82
- package/src/lib/orchestrator/wrap-pass.ts +1 -88
- package/src/runtime/guidance.ts +0 -17
- package/src/runtime/policy.ts +1 -14
- package/src/runtime/runtime-document-manifest.ts +0 -17
- package/src/runtime/wrap-state.ts +0 -4
- package/src/tools/setup/capabilities/openspec.ts +140 -18
- package/src/tools/setup/dynamic-plugins/audiences.ts +7 -39
- package/src/tools/setup/dynamic-plugins/declarations.ts +1 -1
- package/src/tools/setup/dynamic-plugins/hydrate.ts +4 -7
- package/src/tools/setup/engine.ts +10 -141
- package/src/tools/setup/plugin.ts +4 -19
- package/src/tools/setup/plugins/gitignore.ts +1 -7
- package/src/tools/setup/providers/claude.ts +2 -27
- package/src/tools/setup/providers/opencode.ts +0 -10
- package/src/tools/setup/registry.ts +2 -24
- package/src/tools/setup/surface-target.ts +4 -13
- package/src/tools/setup.ts +6 -9
- package/opencode-plugin/src/audience-layer.ts +0 -91
- package/src/runtime/audience-layer.ts +0 -84
- package/src/runtime/opencode-audience-layer.ts +0 -176
- package/src/tools/setup/audience-isolation.fixture.ts +0 -325
- package/src/tools/setup/audience-layer.ts +0 -207
- package/src/tools/setup/audience-registrations.ts +0 -52
- package/src/tools/setup/contribution-ledger.ts +0 -207
- package/src/tools/setup/isolation-diagnostics.ts +0 -139
- package/src/tools/setup/upgrade-sweep.ts +0 -153
|
@@ -3,22 +3,13 @@ import type { Plugin, PluginPolicy, PluginRegistration } from "./plugin";
|
|
|
3
3
|
export interface NormalizedRegistration {
|
|
4
4
|
plugin: Plugin;
|
|
5
5
|
policy: PluginPolicy;
|
|
6
|
-
/** The audience that declared the plugin; `null` for base. */
|
|
7
|
-
audience: string | null;
|
|
8
|
-
/** The declared package a dynamic plugin was loaded from; absent for compiled-in plugins. */
|
|
9
|
-
packageName?: string;
|
|
10
6
|
}
|
|
11
7
|
|
|
12
8
|
export function normalizeRegistration(entry: PluginRegistration): NormalizedRegistration {
|
|
13
9
|
if ("plugin" in entry && "policy" in entry) {
|
|
14
|
-
return {
|
|
15
|
-
plugin: entry.plugin,
|
|
16
|
-
policy: entry.policy,
|
|
17
|
-
audience: entry.audience ?? null,
|
|
18
|
-
...(entry.packageName ? { packageName: entry.packageName } : {}),
|
|
19
|
-
};
|
|
10
|
+
return { plugin: entry.plugin, policy: entry.policy };
|
|
20
11
|
}
|
|
21
|
-
return { plugin: entry, policy: entry.defaultSelected ? "default" : "optional"
|
|
12
|
+
return { plugin: entry, policy: entry.defaultSelected ? "default" : "optional" };
|
|
22
13
|
}
|
|
23
14
|
|
|
24
15
|
export class PluginRegistry {
|
|
@@ -41,19 +32,6 @@ export class PluginRegistry {
|
|
|
41
32
|
return this.entries.map((entry) => ({ ...entry }));
|
|
42
33
|
}
|
|
43
34
|
|
|
44
|
-
/**
|
|
45
|
-
* Compiled-in plugins and top-level declarations: the only registrations a
|
|
46
|
-
* pass that writes a persisted surface may reconcile.
|
|
47
|
-
*/
|
|
48
|
-
baseRegistrations(): NormalizedRegistration[] {
|
|
49
|
-
return this.getEntries().filter((entry) => entry.audience === null);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** The registrations one audience declared; only its launch layer renders them. */
|
|
53
|
-
audienceRegistrations(audience: string): NormalizedRegistration[] {
|
|
54
|
-
return this.getEntries().filter((entry) => entry.audience === audience);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
35
|
getPolicy(pluginId: string): PluginPolicy | undefined {
|
|
58
36
|
return this.entries.find((entry) => entry.plugin.id === pluginId)?.policy;
|
|
59
37
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
|
-
import { audienceLayerDir } from "../../runtime/audience-layer";
|
|
4
3
|
import type { ProjectionTarget, SetupContext } from "./plugin";
|
|
5
4
|
|
|
6
5
|
/**
|
|
@@ -15,9 +14,6 @@ export function projectionTarget(ctx: SetupContext): ProjectionTarget {
|
|
|
15
14
|
"a working-target reconcile needs a Working Repository in scope; none was supplied",
|
|
16
15
|
);
|
|
17
16
|
}
|
|
18
|
-
if (target === "audience-layer" && !ctx.audience) {
|
|
19
|
-
throw new Error("an audience-layer reconcile needs an audience; none was supplied");
|
|
20
|
-
}
|
|
21
17
|
return target;
|
|
22
18
|
}
|
|
23
19
|
|
|
@@ -42,18 +38,13 @@ export function projectionRepoRoot(ctx: SetupContext): string {
|
|
|
42
38
|
}
|
|
43
39
|
|
|
44
40
|
/**
|
|
45
|
-
* The root a Runtime Surface writes into
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* Surface ever computes a path inside one.
|
|
41
|
+
* The root a Runtime Surface writes into. Defined for the companion target
|
|
42
|
+
* only: a working-target pass renders its documents for the Projection Root to
|
|
43
|
+
* place, so no Runtime Surface ever computes a path inside a Working Repository.
|
|
49
44
|
*/
|
|
50
45
|
export function surfaceRoot(ctx: SetupContext): string {
|
|
51
|
-
|
|
52
|
-
if (target === "working") {
|
|
46
|
+
if (projectionTarget(ctx) === "working") {
|
|
53
47
|
throw new Error("a working-target reconcile renders its documents; it writes no surface root");
|
|
54
48
|
}
|
|
55
|
-
if (target === "audience-layer") {
|
|
56
|
-
return ctx.layerRoot ?? audienceLayerDir(ctx.companionPath, ctx.audience!);
|
|
57
|
-
}
|
|
58
49
|
return ctx.companionPath;
|
|
59
50
|
}
|
package/src/tools/setup.ts
CHANGED
|
@@ -61,7 +61,7 @@ export async function applySetupCompatibilities(
|
|
|
61
61
|
companionPath: string,
|
|
62
62
|
config: FrameworkConfig,
|
|
63
63
|
mode: "setup" | "sync",
|
|
64
|
-
plugins: PluginRegistration[] = getActiveDistribution().registry.
|
|
64
|
+
plugins: PluginRegistration[] = getActiveDistribution().registry.getEntries(),
|
|
65
65
|
repoPath?: string,
|
|
66
66
|
scope: SetupScope = config.type === "hub" ? "hub" : "companion",
|
|
67
67
|
): Promise<SetupInstallationOutcome> {
|
|
@@ -73,15 +73,14 @@ export async function applySetupCompatibilities(
|
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
75
|
* The active Agent Runtimes' documents for a Working Repository, rendered
|
|
76
|
-
* without a write
|
|
76
|
+
* without a write. Runs no plugin phase: the working target reconciles
|
|
77
77
|
* declarations only, so nothing is written to the companion on its behalf.
|
|
78
78
|
*/
|
|
79
79
|
export async function renderWorkingRuntimeDocuments(
|
|
80
80
|
companionPath: string,
|
|
81
81
|
config: FrameworkConfig,
|
|
82
82
|
repoPath: string,
|
|
83
|
-
plugins: PluginRegistration[] = getActiveDistribution().registry.
|
|
84
|
-
wrapped?: { audience: string; registrations: PluginRegistration[] },
|
|
83
|
+
plugins: PluginRegistration[] = getActiveDistribution().registry.getAll(),
|
|
85
84
|
): Promise<RenderedRuntimeDocuments> {
|
|
86
85
|
const plan = buildSetupInstallationPlan(config, plugins);
|
|
87
86
|
const ctx: SetupContext = {
|
|
@@ -92,7 +91,7 @@ export async function renderWorkingRuntimeDocuments(
|
|
|
92
91
|
repoPath,
|
|
93
92
|
target: "working",
|
|
94
93
|
};
|
|
95
|
-
return renderRuntimeDocuments(ctx, plugins, plan
|
|
94
|
+
return renderRuntimeDocuments(ctx, plugins, plan);
|
|
96
95
|
}
|
|
97
96
|
|
|
98
97
|
export async function updateProjectGitignore(
|
|
@@ -100,9 +99,7 @@ export async function updateProjectGitignore(
|
|
|
100
99
|
config: FrameworkConfig,
|
|
101
100
|
): Promise<void> {
|
|
102
101
|
const ctx: SetupContext = { companionPath, config, mode: "sync", activeProviders: [] };
|
|
103
|
-
const plugins = getActiveDistribution()
|
|
104
|
-
.registry.baseRegistrations()
|
|
105
|
-
.map((entry) => entry.plugin);
|
|
102
|
+
const plugins = getActiveDistribution().registry.getAll();
|
|
106
103
|
const entries = collectManagedGitignoreEntries(ctx, plugins);
|
|
107
104
|
await writeManagedGitignoreBlock(path.join(companionPath, ".gitignore"), FRAMEWORK_NAME, entries);
|
|
108
105
|
}
|
|
@@ -116,7 +113,7 @@ export async function syncCompanionFiles(
|
|
|
116
113
|
companionPath,
|
|
117
114
|
config,
|
|
118
115
|
"sync",
|
|
119
|
-
getActiveDistribution().registry.
|
|
116
|
+
getActiveDistribution().registry.getAll(),
|
|
120
117
|
repoPath,
|
|
121
118
|
);
|
|
122
119
|
}
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import type { Context } from "@opencode/plugin/promise/plugin";
|
|
2
|
-
|
|
3
|
-
import { repositoryWrappedForOpenCode } from "../../src/opencode/companion-hooks";
|
|
4
|
-
import { currentAudienceLayer } from "../../src/runtime/audience-layer";
|
|
5
|
-
import { MATE_ENV } from "../../src/runtime/env";
|
|
6
|
-
import { parseGuidanceContent } from "../../src/runtime/guidance";
|
|
7
|
-
import {
|
|
8
|
-
readOpenCodeAudienceLayer,
|
|
9
|
-
type OpenCodeLayerContents,
|
|
10
|
-
} from "../../src/runtime/opencode-audience-layer";
|
|
11
|
-
import { readWrapStateSync } from "../../src/runtime/wrap-state";
|
|
12
|
-
import type { LaunchEnvironment } from "./launch-environment";
|
|
13
|
-
|
|
14
|
-
export interface TrustedAudienceLayer {
|
|
15
|
-
root: string;
|
|
16
|
-
contents: OpenCodeLayerContents;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* The audience layer named by a payload the plugin trusts. `env` is the
|
|
21
|
-
* launch environment `pluginLaunchEnvironment` decided on, so a masked
|
|
22
|
-
* payload, a bare session and the shared service all read as `{}` here and
|
|
23
|
-
* never apply a layer.
|
|
24
|
-
*/
|
|
25
|
-
export async function readTrustedAudienceLayer(
|
|
26
|
-
env: LaunchEnvironment,
|
|
27
|
-
): Promise<TrustedAudienceLayer | null> {
|
|
28
|
-
const payload = env[MATE_ENV.guidanceJson];
|
|
29
|
-
if (!payload?.trim()) return null;
|
|
30
|
-
const root = parseGuidanceContent(payload).guidance.audienceLayer;
|
|
31
|
-
if (!root) return null;
|
|
32
|
-
return { root, contents: await readOpenCodeAudienceLayer(root) };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* The layer of the audience the session's checkout is wrapped for. The wrap is
|
|
37
|
-
* resolved from the session's own directory, so the shared service serves each
|
|
38
|
-
* checkout its own audience; the environment never selects one.
|
|
39
|
-
*/
|
|
40
|
-
export async function readWrappedAudienceLayer(
|
|
41
|
-
directory: string,
|
|
42
|
-
companionPath: string,
|
|
43
|
-
): Promise<TrustedAudienceLayer | null> {
|
|
44
|
-
const repoRoot = repositoryWrappedForOpenCode(directory);
|
|
45
|
-
if (!repoRoot) return null;
|
|
46
|
-
const audience = readWrapStateSync(repoRoot).audience;
|
|
47
|
-
if (!audience) return null;
|
|
48
|
-
const root = currentAudienceLayer(companionPath, audience);
|
|
49
|
-
return root ? { root, contents: await readOpenCodeAudienceLayer(root) } : null;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Applies a layer's MCP servers, skills and agents through the plugin API. A
|
|
54
|
-
* new agent id is created by updating it; its prompt goes in `system`.
|
|
55
|
-
*/
|
|
56
|
-
export async function registerAudienceLayer(
|
|
57
|
-
api: Context,
|
|
58
|
-
layer: OpenCodeLayerContents,
|
|
59
|
-
): Promise<void> {
|
|
60
|
-
if (layer.mcpServers.length > 0) {
|
|
61
|
-
await api.mcp.transform((editor) => {
|
|
62
|
-
for (const server of layer.mcpServers) editor.set(server.name, server.config as never);
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (layer.skills.length > 0) {
|
|
67
|
-
await api.skill.transform((editor) => {
|
|
68
|
-
for (const skill of layer.skills) {
|
|
69
|
-
if (editor.get(skill.id)) {
|
|
70
|
-
editor.update(skill.id, (current) => Object.assign(current, skill));
|
|
71
|
-
} else {
|
|
72
|
-
editor.add(skill as never);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (layer.agents.length > 0) {
|
|
79
|
-
await api.agent.transform((editor) => {
|
|
80
|
-
for (const agent of layer.agents) {
|
|
81
|
-
editor.update(agent.id, (current) => {
|
|
82
|
-
const target = current as Record<string, unknown>;
|
|
83
|
-
target.name ??= agent.id;
|
|
84
|
-
if (agent.description) target.description = agent.description;
|
|
85
|
-
target.mode = agent.mode;
|
|
86
|
-
target.system = agent.system;
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
}
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import crypto from "node:crypto";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
|
|
5
|
-
import { FRAMEWORK_NAME } from "./framework";
|
|
6
|
-
import { mateHome } from "./runtime-directory";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Audience Layer locations. A layer holds one audience's plugin contributions
|
|
10
|
-
* for one companion, rendered for managed launches only. It lives under Mate's
|
|
11
|
-
* machine-local state, never inside a repository: a layer inside the companion
|
|
12
|
-
* would be readable by every session with companion access, and could carry
|
|
13
|
-
* values resolved from plugin configuration.
|
|
14
|
-
*
|
|
15
|
-
* <state>/audience-layers/<companion-key>/<audience>/
|
|
16
|
-
* <fingerprint>/ one complete render; never changed once renamed into place
|
|
17
|
-
* current symlink to the newest complete render
|
|
18
|
-
*
|
|
19
|
-
* A launch attaches the fingerprint directory itself, so a later re-render
|
|
20
|
-
* never changes what a running session reads.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
/** Mirrors `AUDIENCE_NAME_PATTERN`; repeated here because `runtime/` imports nothing from core. */
|
|
24
|
-
const SAFE_AUDIENCE = /^[a-z][a-z0-9-]{0,63}$/;
|
|
25
|
-
|
|
26
|
-
export const CURRENT_LAYER_LINK = "current";
|
|
27
|
-
|
|
28
|
-
export function audienceLayersRoot(homeDir?: string): string {
|
|
29
|
-
return path.join(mateHome(homeDir), "state", "audience-layers");
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** A short, stable key for one companion checkout: a hash of its resolved path. */
|
|
33
|
-
export function companionLayerKey(companionPath: string): string {
|
|
34
|
-
return crypto.createHash("sha256").update(path.resolve(companionPath)).digest("hex").slice(0, 16);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** The directory holding one audience's layers for one companion. */
|
|
38
|
-
export function audienceLayerDir(
|
|
39
|
-
companionPath: string,
|
|
40
|
-
audience: string,
|
|
41
|
-
homeDir?: string,
|
|
42
|
-
): string {
|
|
43
|
-
if (!SAFE_AUDIENCE.test(audience)) {
|
|
44
|
-
throw new Error(`"${audience}" is not a valid audience name for a layer directory`);
|
|
45
|
-
}
|
|
46
|
-
return path.join(audienceLayersRoot(homeDir), companionLayerKey(companionPath), audience);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** The newest complete layer of `audience`, or `null` when none was rendered on this machine. */
|
|
50
|
-
export function currentAudienceLayer(
|
|
51
|
-
companionPath: string,
|
|
52
|
-
audience: string,
|
|
53
|
-
homeDir?: string,
|
|
54
|
-
): string | null {
|
|
55
|
-
try {
|
|
56
|
-
const dir = audienceLayerDir(companionPath, audience, homeDir);
|
|
57
|
-
const layer = path.join(dir, fs.readlinkSync(path.join(dir, CURRENT_LAYER_LINK)));
|
|
58
|
-
return fs.existsSync(layer) ? layer : null;
|
|
59
|
-
} catch {
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const MANAGED_MARKER = new RegExp(
|
|
65
|
-
`^<!-- ${FRAMEWORK_NAME}:managed:[^\\n]* (?:start|end) -->$`,
|
|
66
|
-
"gm",
|
|
67
|
-
);
|
|
68
|
-
|
|
69
|
-
/** A layer guidance document's text, managed-block markers removed; empty when absent. */
|
|
70
|
-
export function layerGuidanceText(text: string): string {
|
|
71
|
-
return text
|
|
72
|
-
.replace(MANAGED_MARKER, "")
|
|
73
|
-
.replace(/\n{3,}/g, "\n\n")
|
|
74
|
-
.trim();
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** `file` (`CLAUDE.md`, `AGENTS.md`) of the layer at `root`, markers removed; empty when absent. */
|
|
78
|
-
export function readLayerGuidanceSync(root: string, file: string): string {
|
|
79
|
-
try {
|
|
80
|
-
return layerGuidanceText(fs.readFileSync(path.join(root, file), "utf8"));
|
|
81
|
-
} catch {
|
|
82
|
-
return "";
|
|
83
|
-
}
|
|
84
|
-
}
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
|
|
4
|
-
import { layerGuidanceText } from "./audience-layer";
|
|
5
|
-
import { OPENCODE_MCP_SERVERS_AT } from "./runtime-document-manifest";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Reads an audience layer the OpenCode Runtime Surface wrote, as what the Mate
|
|
9
|
-
* OpenCode plugin applies through the plugin API. The surface re-exports it as
|
|
10
|
-
* its own reader; it lives under `runtime/` only so the import-isolated plugin
|
|
11
|
-
* can reach it. The plugin parses no OpenCode document itself.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
export interface LayerMcpServer {
|
|
15
|
-
name: string;
|
|
16
|
-
/** The entry exactly as the surface wrote it under `mcp.servers`. */
|
|
17
|
-
config: Record<string, unknown>;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface LayerSkill {
|
|
21
|
-
id: string;
|
|
22
|
-
name: string;
|
|
23
|
-
description?: string;
|
|
24
|
-
/** The skill's `SKILL.md`. */
|
|
25
|
-
path: string;
|
|
26
|
-
content: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export type LayerAgentMode = "subagent" | "primary" | "all";
|
|
30
|
-
|
|
31
|
-
export interface LayerAgent {
|
|
32
|
-
id: string;
|
|
33
|
-
description?: string;
|
|
34
|
-
mode: LayerAgentMode;
|
|
35
|
-
/** The agent's prompt: the definition's body. */
|
|
36
|
-
system: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface OpenCodeLayerContents {
|
|
40
|
-
mcpServers: LayerMcpServer[];
|
|
41
|
-
skills: LayerSkill[];
|
|
42
|
-
agents: LayerAgent[];
|
|
43
|
-
/** The layer's guidance sections, managed-block markers removed. */
|
|
44
|
-
guidance: string;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
48
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function splitFrontmatter(content: string): { fields: string; body: string } {
|
|
52
|
-
const match = content.match(/^---\s*\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
|
|
53
|
-
return match
|
|
54
|
-
? { fields: match[1]!, body: content.slice(match[0].length) }
|
|
55
|
-
: { fields: "", body: content };
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function frontmatterScalarFromFields(fields: string, key: string): string | undefined {
|
|
59
|
-
const value = fields.match(new RegExp(`^${key}:\\s*(.+)$`, "m"))?.[1]?.trim();
|
|
60
|
-
return value?.replace(
|
|
61
|
-
/^(?:"([\s\S]*)"|'([\s\S]*)')$/,
|
|
62
|
-
(_all, doubleQuoted?: string, singleQuoted?: string) => doubleQuoted ?? singleQuoted ?? "",
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** One scalar of a simple YAML frontmatter, quotes removed. */
|
|
67
|
-
export function frontmatterScalar(content: string, key: string): string | undefined {
|
|
68
|
-
return frontmatterScalarFromFields(splitFrontmatter(content).fields, key);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** An id OpenCode accepts for a skill or agent name. */
|
|
72
|
-
export function contributionId(name: string, fallback: string): string {
|
|
73
|
-
return (
|
|
74
|
-
name
|
|
75
|
-
.toLowerCase()
|
|
76
|
-
.replace(/[^a-z0-9_-]+/g, "-")
|
|
77
|
-
.replace(/^-+|-+$/g, "") || fallback
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
async function findSkillFiles(directory: string): Promise<string[]> {
|
|
82
|
-
try {
|
|
83
|
-
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
84
|
-
const nested = await Promise.all(
|
|
85
|
-
entries.map((entry) => {
|
|
86
|
-
const entryPath = path.join(directory, entry.name);
|
|
87
|
-
if (entry.isFile() && entry.name === "SKILL.md") return Promise.resolve([entryPath]);
|
|
88
|
-
return entry.isDirectory() ? findSkillFiles(entryPath) : Promise.resolve([]);
|
|
89
|
-
}),
|
|
90
|
-
);
|
|
91
|
-
return nested.flat().toSorted();
|
|
92
|
-
} catch {
|
|
93
|
-
return [];
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** Every `SKILL.md` under `roots`; an unreadable skill is skipped, never fatal. */
|
|
98
|
-
export async function loadSkillInfos(roots: readonly string[]): Promise<LayerSkill[]> {
|
|
99
|
-
const files = (await Promise.all(roots.map(findSkillFiles))).flat();
|
|
100
|
-
const skills = await Promise.all(
|
|
101
|
-
files.map(async (filePath): Promise<LayerSkill | null> => {
|
|
102
|
-
try {
|
|
103
|
-
const content = await fs.readFile(filePath, "utf8");
|
|
104
|
-
const { fields } = splitFrontmatter(content);
|
|
105
|
-
const name =
|
|
106
|
-
frontmatterScalarFromFields(fields, "name") || path.basename(path.dirname(filePath));
|
|
107
|
-
const description = frontmatterScalarFromFields(fields, "description");
|
|
108
|
-
return {
|
|
109
|
-
id: contributionId(name, "mate-skill"),
|
|
110
|
-
name,
|
|
111
|
-
...(description ? { description } : {}),
|
|
112
|
-
path: filePath,
|
|
113
|
-
content,
|
|
114
|
-
};
|
|
115
|
-
} catch {
|
|
116
|
-
return null;
|
|
117
|
-
}
|
|
118
|
-
}),
|
|
119
|
-
);
|
|
120
|
-
return skills.filter((skill) => skill !== null);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
async function readMcpServers(root: string): Promise<LayerMcpServer[]> {
|
|
124
|
-
const parsed = await fs
|
|
125
|
-
.readFile(path.join(root, ".opencode", "opencode.json"), "utf8")
|
|
126
|
-
.then((text) => JSON.parse(text) as unknown)
|
|
127
|
-
.catch(() => null);
|
|
128
|
-
let servers: unknown = parsed;
|
|
129
|
-
for (const key of OPENCODE_MCP_SERVERS_AT) servers = isRecord(servers) ? servers[key] : undefined;
|
|
130
|
-
if (!isRecord(servers)) return [];
|
|
131
|
-
return Object.entries(servers)
|
|
132
|
-
.filter((entry): entry is [string, Record<string, unknown>] => isRecord(entry[1]))
|
|
133
|
-
.map(([name, config]) => ({ name, config }))
|
|
134
|
-
.toSorted((left, right) => left.name.localeCompare(right.name));
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function agentMode(value: string | undefined): LayerAgentMode {
|
|
138
|
-
return value === "subagent" || value === "primary" ? value : "all";
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
async function readAgents(root: string): Promise<LayerAgent[]> {
|
|
142
|
-
const dir = path.join(root, ".opencode", "agents");
|
|
143
|
-
const names = await fs.readdir(dir).catch(() => [] as string[]);
|
|
144
|
-
const agents = await Promise.all(
|
|
145
|
-
names
|
|
146
|
-
.filter((name) => name.endsWith(".md"))
|
|
147
|
-
.toSorted()
|
|
148
|
-
.map(async (name): Promise<LayerAgent | null> => {
|
|
149
|
-
const content = await fs.readFile(path.join(dir, name), "utf8").catch(() => null);
|
|
150
|
-
if (content === null) return null;
|
|
151
|
-
const { fields, body } = splitFrontmatter(content);
|
|
152
|
-
const description = frontmatterScalarFromFields(fields, "description");
|
|
153
|
-
return {
|
|
154
|
-
id: contributionId(path.basename(name, ".md"), "mate-agent"),
|
|
155
|
-
...(description ? { description } : {}),
|
|
156
|
-
mode: agentMode(frontmatterScalarFromFields(fields, "mode")),
|
|
157
|
-
system: body.trim(),
|
|
158
|
-
};
|
|
159
|
-
}),
|
|
160
|
-
);
|
|
161
|
-
return agents.filter((agent) => agent !== null);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
async function readGuidance(root: string): Promise<string> {
|
|
165
|
-
return layerGuidanceText(await fs.readFile(path.join(root, "AGENTS.md"), "utf8").catch(() => ""));
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
export async function readOpenCodeAudienceLayer(root: string): Promise<OpenCodeLayerContents> {
|
|
169
|
-
const [mcpServers, skills, agents, guidance] = await Promise.all([
|
|
170
|
-
readMcpServers(root),
|
|
171
|
-
loadSkillInfos([path.join(root, ".agents", "skills")]),
|
|
172
|
-
readAgents(root),
|
|
173
|
-
readGuidance(root),
|
|
174
|
-
]);
|
|
175
|
-
return { mcpServers, skills, agents, guidance };
|
|
176
|
-
}
|