@uniqbit/mate-core 0.15.1 → 0.15.2-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 +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/tools/setup/capabilities/openspec.ts +7 -50
- 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
|
"",
|
|
@@ -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
|
}
|
|
@@ -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) ?? []),
|