@uniqbit/mate-core 0.15.1 → 0.15.2-canary.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 +1 -1
- package/src/cli/commands/report/adapter.ts +73 -0
- package/src/cli/commands/report/contract.ts +257 -0
- package/src/cli/commands/report/delivery.ts +79 -0
- package/src/cli/commands/report/index.ts +171 -110
- package/src/cli/commands/report/renderer.ts +228 -73
- package/src/cli/commands/report/types.ts +77 -0
- package/src/cli/main.ts +7 -6
- package/src/cli/plugin-commands.ts +27 -2
- package/src/cli/usage.ts +3 -1
- package/src/templates/root/TEMPLATE_AGENTS.md +1 -1
- package/src/tools/setup/capabilities/openspec.ts +13 -52
- package/src/tools/setup/capabilities/tokensave.ts +37 -4
- package/src/tools/setup/mate.ts +124 -0
- package/src/tools/setup/plugins/gitignore.ts +1 -0
|
@@ -36,7 +36,84 @@ export interface ReportData {
|
|
|
36
36
|
netSpend: number;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
export const REPORT_DOCUMENT_VERSION = 1 as const;
|
|
40
|
+
|
|
41
|
+
export type ReportValue = string | number | boolean | null;
|
|
42
|
+
|
|
43
|
+
export interface ReportKeyValue {
|
|
44
|
+
label: string;
|
|
45
|
+
value: ReportValue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ReportMetric {
|
|
49
|
+
label: string;
|
|
50
|
+
value: ReportValue;
|
|
51
|
+
detail?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ReportStatus {
|
|
55
|
+
label: string;
|
|
56
|
+
status: string;
|
|
57
|
+
detail?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface ReportSectionBase {
|
|
61
|
+
id: string;
|
|
62
|
+
title: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface ReportMetadataSection extends ReportSectionBase {
|
|
66
|
+
type: "metadata";
|
|
67
|
+
items: ReportKeyValue[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface ReportMetricsSection extends ReportSectionBase {
|
|
71
|
+
type: "metrics";
|
|
72
|
+
items: ReportMetric[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface ReportKeyValueSection extends ReportSectionBase {
|
|
76
|
+
type: "key-value";
|
|
77
|
+
items: ReportKeyValue[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface ReportTableSection extends ReportSectionBase {
|
|
81
|
+
type: "table";
|
|
82
|
+
columns: string[];
|
|
83
|
+
rows: ReportValue[][];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface ReportStatusesSection extends ReportSectionBase {
|
|
87
|
+
type: "statuses";
|
|
88
|
+
items: ReportStatus[];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface ReportTextSection extends ReportSectionBase {
|
|
92
|
+
type: "text";
|
|
93
|
+
content: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export type ReportSection =
|
|
97
|
+
| ReportMetadataSection
|
|
98
|
+
| ReportMetricsSection
|
|
99
|
+
| ReportKeyValueSection
|
|
100
|
+
| ReportTableSection
|
|
101
|
+
| ReportStatusesSection
|
|
102
|
+
| ReportTextSection;
|
|
103
|
+
|
|
104
|
+
export interface ReportDocument {
|
|
105
|
+
version: typeof REPORT_DOCUMENT_VERSION;
|
|
106
|
+
title: string;
|
|
107
|
+
generatedAt: string;
|
|
108
|
+
period?: string;
|
|
109
|
+
context?: string;
|
|
110
|
+
metadata: ReportKeyValue[];
|
|
111
|
+
summary: ReportMetric[];
|
|
112
|
+
sections: ReportSection[];
|
|
113
|
+
}
|
|
114
|
+
|
|
39
115
|
export interface ReportOptions {
|
|
40
116
|
days: number;
|
|
41
117
|
json: boolean;
|
|
118
|
+
input?: string;
|
|
42
119
|
}
|
package/src/cli/main.ts
CHANGED
|
@@ -51,17 +51,18 @@ export async function main(argv = process.argv, deps: MainDeps = mainDeps): Prom
|
|
|
51
51
|
return;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
// Companion-declared plugins register before cap-command detection and
|
|
55
|
+
// before help text is printed, so their commands route like compiled-in
|
|
56
|
+
// ones (including MCP servers whose command is their own cap subcommand)
|
|
57
|
+
// and show up in `mate help`. Diagnostics stay on stderr; a missing or
|
|
58
|
+
// ambiguous companion makes this a no-op.
|
|
59
|
+
await deps.hydrateDynamicPlugins();
|
|
60
|
+
|
|
54
61
|
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
55
62
|
console.log(usage());
|
|
56
63
|
return;
|
|
57
64
|
}
|
|
58
65
|
|
|
59
|
-
// Companion-declared plugins register before cap-command detection so
|
|
60
|
-
// their commands route like compiled-in ones (including MCP servers whose
|
|
61
|
-
// command is their own cap subcommand). Diagnostics stay on stderr; a
|
|
62
|
-
// missing or ambiguous companion makes this a no-op.
|
|
63
|
-
await deps.hydrateDynamicPlugins();
|
|
64
|
-
|
|
65
66
|
// Plugin commands (`mate cap <namespace> <command>`) own their stdout (an
|
|
66
67
|
// MCP server speaks JSON-RPC over it), so banners and background chatter
|
|
67
68
|
// are suppressed for them.
|
|
@@ -95,11 +95,36 @@ export async function ensureCapabilityEnabled(
|
|
|
95
95
|
return false;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
function formatCommand(
|
|
99
|
+
distributionName: string,
|
|
100
|
+
namespace: string,
|
|
101
|
+
command: PluginCliCommand,
|
|
102
|
+
): string {
|
|
103
|
+
return ` ${distributionName} cap ${namespace} ${command.name} ${command.description}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
98
106
|
function usageFor(plugin: Plugin): string {
|
|
99
107
|
const distributionName = frameworkCommandName();
|
|
100
108
|
const namespace = namespaceOf(plugin);
|
|
101
|
-
const lines = (plugin.cliCommands ?? []).map(
|
|
102
|
-
(
|
|
109
|
+
const lines = (plugin.cliCommands ?? []).map((command) =>
|
|
110
|
+
formatCommand(distributionName, namespace, command),
|
|
103
111
|
);
|
|
104
112
|
return ["Available commands:", ...lines].join("\n");
|
|
105
113
|
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Cap-command lines contributed by every registered plugin that declares
|
|
117
|
+
* `cliCommands` (compiled-in or companion-declared/dynamic), formatted as
|
|
118
|
+
* ` <n> cap <namespace> <name> <description>`. Used by `usage()` so plugin
|
|
119
|
+
* commands show up in `mate help` alongside framework builtins.
|
|
120
|
+
*/
|
|
121
|
+
export function pluginCliCommandLines(): string[] {
|
|
122
|
+
const distributionName = frameworkCommandName();
|
|
123
|
+
return getActiveDistribution()
|
|
124
|
+
.registry.getAll()
|
|
125
|
+
.flatMap((plugin) =>
|
|
126
|
+
(plugin.cliCommands ?? []).map((command) =>
|
|
127
|
+
formatCommand(distributionName, namespaceOf(plugin), command),
|
|
128
|
+
),
|
|
129
|
+
);
|
|
130
|
+
}
|
package/src/cli/usage.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getActiveDistribution } from "../distribution";
|
|
2
2
|
import { FRAMEWORK_NAME, frameworkCommandName } from "../framework";
|
|
3
|
+
import { pluginCliCommandLines } from "./plugin-commands";
|
|
3
4
|
|
|
4
5
|
export function usage(): string {
|
|
5
6
|
const n = frameworkCommandName();
|
|
@@ -17,13 +18,14 @@ export function usage(): string {
|
|
|
17
18
|
` ${n} companion tui`,
|
|
18
19
|
` ${n} artifact finish <change-name> [--type openspec] [--force] [--no-push] [--json]`,
|
|
19
20
|
` ${n} doctor`,
|
|
20
|
-
` ${n} report [--days N] [--json]`,
|
|
21
|
+
` ${n} report [--days N] [--input FILE|-] [--json]`,
|
|
21
22
|
` ${n} config`,
|
|
22
23
|
` ${n} claude [args...] (use -- --no-git to bypass companion Git sync)`,
|
|
23
24
|
` ${n} opencode [args...] (use -- --no-git to bypass companion Git sync)`,
|
|
24
25
|
` ${n} cap openspec <subcommand> [args...]`,
|
|
25
26
|
` ${n} cap graphify <subcommand> [args...]`,
|
|
26
27
|
` ${n} cap index [--graphify] [--tokensave]`,
|
|
28
|
+
...pluginCliCommandLines(),
|
|
27
29
|
` ${n} update`,
|
|
28
30
|
` ${n} update --check`,
|
|
29
31
|
"",
|
|
@@ -8,7 +8,7 @@ This block is kept for AGENTS.md compatibility and must not restate that policy.
|
|
|
8
8
|
## Agent Notes
|
|
9
9
|
|
|
10
10
|
- Use absolute paths when a tool needs a file path. Do not create literal `$MATE_REPO_PATH` or `$MATE_ARTIFACT_PATH` directories.
|
|
11
|
-
- When reporting information to me, be extremely concise and sacrifice grammar for the sake of concision. Apply this same preference to JSDoc
|
|
11
|
+
- When reporting information to me, be extremely concise and sacrifice grammar for the sake of concision. Apply this same preference to JSDoc.
|
|
12
12
|
- Never add a `Co-Authored-By: <model>` trailer or model-attribution footer to commit messages.
|
|
13
13
|
- Never commit, push, or open a pull request in the working repo (`$MATE_REPO_PATH`) unless the user explicitly asks for it.
|
|
14
14
|
|
|
@@ -3,7 +3,6 @@ import fs from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { promisify } from "node:util";
|
|
5
5
|
|
|
6
|
-
import { frameworkCommandName } from "../../../framework";
|
|
7
6
|
import { getOpenSpecSchemaSelection } from "../../../lib/orchestrator/setup-compatibilities";
|
|
8
7
|
import { fetchPublicPackageVersion } from "../../../lib/public-npm";
|
|
9
8
|
import { isNewer } from "../../../lib/update-checker";
|
|
@@ -18,6 +17,9 @@ import {
|
|
|
18
17
|
runCommand,
|
|
19
18
|
runCommandSilently,
|
|
20
19
|
} from "../utils";
|
|
20
|
+
import { applyMateSkills, teardownMateSkills } from "../mate";
|
|
21
|
+
|
|
22
|
+
export { MATE_ARTIFACT_SKILLS, MATE_SKILLS, deployMateSkillDir } from "../mate";
|
|
21
23
|
|
|
22
24
|
const execFileAsync = promisify(execFileCb);
|
|
23
25
|
|
|
@@ -30,14 +32,8 @@ export const OPENSPEC_SKILLS = [
|
|
|
30
32
|
|
|
31
33
|
// Mate-authored skills (not generated by the openspec CLI) that ship alongside the
|
|
32
34
|
// openspec workflow skills and are installed per active provider.
|
|
33
|
-
export const MATE_ARTIFACT_SKILLS = ["mate-artifact-finish"] as const;
|
|
34
35
|
const LEGACY_CLAUDE_STATE_PATTERN = /^mate-artifact-finish\..+\.json$/;
|
|
35
36
|
|
|
36
|
-
const MATE_SKILLS_SOURCE = path.join(
|
|
37
|
-
import.meta.dirname,
|
|
38
|
-
"../../../templates/capabilities/openspec-cap/mate-skills",
|
|
39
|
-
);
|
|
40
|
-
|
|
41
37
|
const MATE_V1_SCHEMA_SOURCE = path.join(
|
|
42
38
|
import.meta.dirname,
|
|
43
39
|
"../../../templates/capabilities/openspec-cap/mate-v1",
|
|
@@ -110,24 +106,14 @@ export async function teardownOpenspecSkills(
|
|
|
110
106
|
skillsDir: string,
|
|
111
107
|
companionPath: string,
|
|
112
108
|
): Promise<void> {
|
|
113
|
-
for (const skill of
|
|
114
|
-
try {
|
|
115
|
-
await fs.rm(path.join(skillsDir, skill), { recursive: true, force: true });
|
|
116
|
-
} catch {
|
|
117
|
-
/* not present */
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
await pruneEmptyAncestors(skillsDir, companionPath);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
async function teardownMateOpenspecSkills(skillsDir: string, companionPath: string): Promise<void> {
|
|
124
|
-
for (const skill of MATE_ARTIFACT_SKILLS) {
|
|
109
|
+
for (const skill of OPENSPEC_SKILLS) {
|
|
125
110
|
try {
|
|
126
111
|
await fs.rm(path.join(skillsDir, skill), { recursive: true, force: true });
|
|
127
112
|
} catch {
|
|
128
113
|
/* not present */
|
|
129
114
|
}
|
|
130
115
|
}
|
|
116
|
+
await teardownMateSkills(skillsDir, companionPath);
|
|
131
117
|
await pruneEmptyAncestors(skillsDir, companionPath);
|
|
132
118
|
}
|
|
133
119
|
|
|
@@ -135,42 +121,13 @@ async function teardownToolRuntime(companionPath: string, tool: OpenSpecTool): P
|
|
|
135
121
|
await teardownOpenspecSkills(getSkillsDir(companionPath, tool), companionPath);
|
|
136
122
|
}
|
|
137
123
|
|
|
138
|
-
const MATE_COMMAND_PLACEHOLDER = "{{MATE_COMMAND}}";
|
|
139
|
-
|
|
140
|
-
// Like mergeDir, but renders the invocation-name placeholder at deploy time.
|
|
141
|
-
// Files without the placeholder are copied byte-identical.
|
|
142
|
-
export async function deployMateSkillDir(src: string, dest: string): Promise<void> {
|
|
143
|
-
await fs.mkdir(dest, { recursive: true });
|
|
144
|
-
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
145
|
-
for (const entry of entries) {
|
|
146
|
-
const srcPath = path.join(src, entry.name);
|
|
147
|
-
const destPath = path.join(dest, entry.name);
|
|
148
|
-
if (entry.isDirectory()) {
|
|
149
|
-
await deployMateSkillDir(srcPath, destPath);
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
const content = await fs.readFile(srcPath, "utf8");
|
|
153
|
-
if (content.includes(MATE_COMMAND_PLACEHOLDER)) {
|
|
154
|
-
await fs.writeFile(
|
|
155
|
-
destPath,
|
|
156
|
-
content.replaceAll(MATE_COMMAND_PLACEHOLDER, frameworkCommandName()),
|
|
157
|
-
"utf8",
|
|
158
|
-
);
|
|
159
|
-
} else {
|
|
160
|
-
await fs.copyFile(srcPath, destPath);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
124
|
async function applyMateOpenspecSkills(
|
|
166
125
|
companionPath: string,
|
|
167
126
|
tools: OpenSpecTool[],
|
|
168
127
|
): Promise<void> {
|
|
169
128
|
for (const tool of tools) {
|
|
170
129
|
const skillsDir = getSkillsDir(companionPath, tool);
|
|
171
|
-
|
|
172
|
-
await deployMateSkillDir(path.join(MATE_SKILLS_SOURCE, skill), path.join(skillsDir, skill));
|
|
173
|
-
}
|
|
130
|
+
await applyMateSkills(skillsDir);
|
|
174
131
|
}
|
|
175
132
|
}
|
|
176
133
|
|
|
@@ -194,7 +151,7 @@ async function reconcileOpenSpecTools(
|
|
|
194
151
|
await applyMateOpenspecSkills(ctx.companionPath, tools);
|
|
195
152
|
} else {
|
|
196
153
|
for (const tool of tools) {
|
|
197
|
-
await
|
|
154
|
+
await teardownMateSkills(getSkillsDir(ctx.companionPath, tool), ctx.companionPath);
|
|
198
155
|
}
|
|
199
156
|
}
|
|
200
157
|
}
|
|
@@ -373,8 +330,12 @@ export function createOpenspecPlugin(deps: OpenSpecPluginDeps = {}): CapabilityP
|
|
|
373
330
|
],
|
|
374
331
|
async apply(ctx) {
|
|
375
332
|
const tools = deriveOpenSpecTools(ctx.activeProviders);
|
|
376
|
-
if (tools.length > 0
|
|
377
|
-
await
|
|
333
|
+
if (ctx.mode === "setup" || tools.length > 0) {
|
|
334
|
+
if (await ensureOpenSpecInstalled(ctx, depsWithDefaults)) {
|
|
335
|
+
if (tools.length > 0) {
|
|
336
|
+
await reconcileOpenSpecTools(ctx, depsWithDefaults.runCommand);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
378
339
|
}
|
|
379
340
|
await reconcileOpenSpecSchema(ctx);
|
|
380
341
|
},
|
|
@@ -9,10 +9,11 @@ export { TOKENSAVE_WORKING_REPO_EXCLUDE_ENTRIES } from "./tokensave-shared";
|
|
|
9
9
|
|
|
10
10
|
export const TOKENSAVE_SUPPORTED_AGENTS = new Set(["claude", "opencode"]);
|
|
11
11
|
export const TOKENSAVE_STORE_DIR = ".tokensave";
|
|
12
|
+
export const TOKENSAVE_MIN_RUST_VERSION = "1.91.0";
|
|
12
13
|
const TOKENSAVE_CLAUDE_MD_MARKER = "## MANDATORY: No Explore Agents When Tokensave Is Available";
|
|
13
14
|
const TOKENSAVE_STORE_EXCLUDE_ENTRY = `${TOKENSAVE_STORE_DIR}/`;
|
|
14
15
|
const TOKENSAVE_BREW_INSTALL_CMD = "brew install aovestdipaperino/tap/tokensave";
|
|
15
|
-
const TOKENSAVE_CARGO_INSTALL_CMD = "cargo install tokensave";
|
|
16
|
+
const TOKENSAVE_CARGO_INSTALL_CMD = "cargo install --locked tokensave";
|
|
16
17
|
const TOKENSAVE_SCOOP_INSTALL_CMD =
|
|
17
18
|
"scoop bucket add tokensave https://github.com/aovestdipaperino/scoop-bucket && scoop install tokensave";
|
|
18
19
|
|
|
@@ -53,6 +54,17 @@ export const tokensaveDeps = {
|
|
|
53
54
|
async runShellCommand(command: string): Promise<void> {
|
|
54
55
|
await runShellCommand(command);
|
|
55
56
|
},
|
|
57
|
+
rustcVersion(): TokensaveRunResult {
|
|
58
|
+
const result = spawnSync("rustc", ["--version"], {
|
|
59
|
+
encoding: "utf8",
|
|
60
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
61
|
+
});
|
|
62
|
+
return {
|
|
63
|
+
ok: result.status === 0 && !result.error,
|
|
64
|
+
stderr: result.stderr ?? "",
|
|
65
|
+
stdout: result.stdout ?? "",
|
|
66
|
+
};
|
|
67
|
+
},
|
|
56
68
|
platform(): NodeJS.Platform {
|
|
57
69
|
return process.platform;
|
|
58
70
|
},
|
|
@@ -148,6 +160,22 @@ function installTokensaveAgentIntegrations(providers: string[], cwd: string): vo
|
|
|
148
160
|
}
|
|
149
161
|
}
|
|
150
162
|
|
|
163
|
+
function getCargoRustVersionError(): string | undefined {
|
|
164
|
+
const result = tokensaveDeps.rustcVersion();
|
|
165
|
+
const version = `${result.stdout}\n${result.stderr}`.match(/\brustc\s+(\d+)\.(\d+)\.(\d+)/);
|
|
166
|
+
if (!result.ok || !version) {
|
|
167
|
+
return `TokenSave requires Rust ${TOKENSAVE_MIN_RUST_VERSION} or newer. Install or upgrade Rust via https://rustup.rs.`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const installed = version.slice(1, 4).map(Number);
|
|
171
|
+
const minimum = TOKENSAVE_MIN_RUST_VERSION.split(".").map(Number);
|
|
172
|
+
const firstDifference = installed.findIndex((part, index) => part !== minimum[index]);
|
|
173
|
+
if (firstDifference !== -1 && installed[firstDifference] < minimum[firstDifference]) {
|
|
174
|
+
return `TokenSave requires Rust ${TOKENSAVE_MIN_RUST_VERSION} or newer, but rustc ${version[1]}.${version[2]}.${version[3]} is installed. Upgrade Rust via https://rustup.rs.`;
|
|
175
|
+
}
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
|
|
151
179
|
function getTokensaveInstallPlan(): { command: string; run: () => Promise<void> } | undefined {
|
|
152
180
|
const pathValue = tokensaveDeps.pathValue();
|
|
153
181
|
const platform = tokensaveDeps.platform();
|
|
@@ -169,7 +197,11 @@ function getTokensaveInstallPlan(): { command: string; run: () => Promise<void>
|
|
|
169
197
|
if (tokensaveDeps.isCommandOnPath("cargo", pathValue)) {
|
|
170
198
|
return {
|
|
171
199
|
command: TOKENSAVE_CARGO_INSTALL_CMD,
|
|
172
|
-
run: () =>
|
|
200
|
+
run: async () => {
|
|
201
|
+
const rustVersionError = getCargoRustVersionError();
|
|
202
|
+
if (rustVersionError) throw new Error(rustVersionError);
|
|
203
|
+
await tokensaveDeps.runCommand("cargo", ["install", "--locked", "tokensave"]);
|
|
204
|
+
},
|
|
173
205
|
};
|
|
174
206
|
}
|
|
175
207
|
|
|
@@ -192,9 +224,10 @@ export async function ensureTokensaveInstalled(repoPath: string): Promise<boolea
|
|
|
192
224
|
process.stdout.write(`tokensave binary not found. Installing with:\n ${installPlan.command}\n`);
|
|
193
225
|
try {
|
|
194
226
|
await installPlan.run();
|
|
195
|
-
} catch {
|
|
227
|
+
} catch (error) {
|
|
228
|
+
const detail = error instanceof Error ? `: ${error.message}` : "";
|
|
196
229
|
process.stderr.write(
|
|
197
|
-
`tokensave: install failed - install manually with \`${installPlan.command}\`\n`,
|
|
230
|
+
`tokensave: install failed${detail} - install manually with \`${installPlan.command}\`\n`,
|
|
198
231
|
);
|
|
199
232
|
return false;
|
|
200
233
|
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { frameworkCommandName } from "../../framework";
|
|
5
|
+
import { pruneEmptyAncestors } from "./utils";
|
|
6
|
+
|
|
7
|
+
export const MATE_ARTIFACT_SKILLS = ["mate-artifact-finish"] as const;
|
|
8
|
+
export const MATE_SKILLS = ["mate-artifact-finish", "mate-create-report"] as const;
|
|
9
|
+
|
|
10
|
+
const MATE_COMMAND_PLACEHOLDER = "{{MATE_COMMAND}}";
|
|
11
|
+
const MATE_SKILLS_SOURCE = path.join(
|
|
12
|
+
import.meta.dirname,
|
|
13
|
+
"../../templates/capabilities/openspec-cap/mate-skills",
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
const MATE_CREATE_REPORT_SKILL = `---
|
|
17
|
+
name: mate-create-report
|
|
18
|
+
description: Create a browser-first Mate report from an explicit structured ReportDocument. Use when a skill needs to present supplied metrics, tables, statuses, metadata, or narrative text.
|
|
19
|
+
allowed-tools: Bash({{MATE_COMMAND}}:*)
|
|
20
|
+
license: MIT
|
|
21
|
+
compatibility: Requires the {{MATE_COMMAND}} CLI and the OpenSpec capability enabled.
|
|
22
|
+
metadata:
|
|
23
|
+
author: mate
|
|
24
|
+
version: "1.0"
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
Create a report only from structured data supplied by the calling skill. Do not infer missing facts from conversation state.
|
|
28
|
+
|
|
29
|
+
## Contract
|
|
30
|
+
|
|
31
|
+
Write a JSON document with this shape:
|
|
32
|
+
|
|
33
|
+
\`\`\`json
|
|
34
|
+
{
|
|
35
|
+
"version": 1,
|
|
36
|
+
"title": "Report title",
|
|
37
|
+
"generatedAt": "2026-01-01T00:00:00Z",
|
|
38
|
+
"period": "Optional reporting period",
|
|
39
|
+
"context": "Optional context",
|
|
40
|
+
"metadata": [{ "label": "Owner", "value": "acme" }],
|
|
41
|
+
"summary": [{ "label": "Requests", "value": 42, "detail": "Optional detail" }],
|
|
42
|
+
"sections": [
|
|
43
|
+
{ "id": "notes", "title": "Notes", "type": "text", "content": "Narrative text." },
|
|
44
|
+
{
|
|
45
|
+
"id": "results",
|
|
46
|
+
"title": "Results",
|
|
47
|
+
"type": "table",
|
|
48
|
+
"columns": ["Name", "Value"],
|
|
49
|
+
"rows": [["Example", 1]]
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
\`\`\`
|
|
54
|
+
|
|
55
|
+
Supported section types are \`metadata\`, \`metrics\`, \`key-value\`, \`table\`, \`statuses\`, and \`text\`. Use only strings, numbers, booleans, and null values. Section IDs must be unique. Validate required fields before invoking the CLI.
|
|
56
|
+
|
|
57
|
+
## Invocation
|
|
58
|
+
|
|
59
|
+
1. Serialize the complete document to a temporary file or pipe it to stdin.
|
|
60
|
+
2. Run \`{{MATE_COMMAND}} report --input <file-or->\`.
|
|
61
|
+
3. Add \`--json\` when the caller needs normalized JSON instead of browser delivery.
|
|
62
|
+
|
|
63
|
+
The default path writes self-contained HTML to a unique OS temporary directory and opens it in the default browser. The report includes a visible "Print / Save as PDF" control that calls the browser's native print dialog. If HTML delivery fails, the CLI warns on stderr and emits the complete report document as JSON on stdout.
|
|
64
|
+
|
|
65
|
+
The built-in \`{{MATE_COMMAND}} report\` path collects Mate usage data and adapts it to the same contract and renderer.
|
|
66
|
+
`;
|
|
67
|
+
|
|
68
|
+
function currentMateCommand(): string {
|
|
69
|
+
try {
|
|
70
|
+
return frameworkCommandName();
|
|
71
|
+
} catch {
|
|
72
|
+
return "mate";
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function deployMateSkillDir(src: string, dest: string): Promise<void> {
|
|
77
|
+
await fs.mkdir(dest, { recursive: true });
|
|
78
|
+
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
79
|
+
for (const entry of entries) {
|
|
80
|
+
const srcPath = path.join(src, entry.name);
|
|
81
|
+
const destPath = path.join(dest, entry.name);
|
|
82
|
+
if (entry.isDirectory()) {
|
|
83
|
+
await deployMateSkillDir(srcPath, destPath);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const content = await fs.readFile(srcPath, "utf8");
|
|
87
|
+
if (content.includes(MATE_COMMAND_PLACEHOLDER)) {
|
|
88
|
+
await fs.writeFile(
|
|
89
|
+
destPath,
|
|
90
|
+
content.replaceAll(MATE_COMMAND_PLACEHOLDER, currentMateCommand()),
|
|
91
|
+
"utf8",
|
|
92
|
+
);
|
|
93
|
+
} else {
|
|
94
|
+
await fs.copyFile(srcPath, destPath);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function applyMateSkills(skillsDir: string): Promise<void> {
|
|
100
|
+
for (const skill of MATE_SKILLS) {
|
|
101
|
+
const destination = path.join(skillsDir, skill);
|
|
102
|
+
if (skill === "mate-create-report") {
|
|
103
|
+
await fs.mkdir(destination, { recursive: true });
|
|
104
|
+
await fs.writeFile(
|
|
105
|
+
path.join(destination, "SKILL.md"),
|
|
106
|
+
MATE_CREATE_REPORT_SKILL.replaceAll(MATE_COMMAND_PLACEHOLDER, currentMateCommand()),
|
|
107
|
+
"utf8",
|
|
108
|
+
);
|
|
109
|
+
} else {
|
|
110
|
+
await deployMateSkillDir(path.join(MATE_SKILLS_SOURCE, skill), destination);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function teardownMateSkills(skillsDir: string, companionPath: string): Promise<void> {
|
|
116
|
+
for (const skill of MATE_SKILLS) {
|
|
117
|
+
try {
|
|
118
|
+
await fs.rm(path.join(skillsDir, skill), { recursive: true, force: true });
|
|
119
|
+
} catch {
|
|
120
|
+
/* not present */
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
await pruneEmptyAncestors(skillsDir, companionPath);
|
|
124
|
+
}
|
|
@@ -64,6 +64,7 @@ export function collectManagedGitignoreEntries(ctx: SetupContext, plugins: Plugi
|
|
|
64
64
|
const entries = [
|
|
65
65
|
"node_modules/",
|
|
66
66
|
`.${FRAMEWORK_NAME}/dependencies/`,
|
|
67
|
+
".mcp.json*",
|
|
67
68
|
...plugins
|
|
68
69
|
.filter((p) => p.kind !== "root" && (p.isEnabled(ctx.config) || p.persistGitignoreEntries))
|
|
69
70
|
.flatMap((p) => p.gitignoreEntries?.(ctx) ?? []),
|