@uniqbit/mate-core 0.15.0 → 0.15.1-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.
Files changed (27) hide show
  1. package/package.json +1 -1
  2. package/src/cli/commands/doctor.ts +4 -4
  3. package/src/cli/commands/report/index.ts +5 -3
  4. package/src/index.ts +1 -0
  5. package/src/lib/context-mode-package.ts +94 -0
  6. package/src/lib/opencode-plugin-package.ts +27 -30
  7. package/src/lib/orchestrator/adapters/claude.ts +20 -0
  8. package/src/lib/orchestrator/adapters/opencode.ts +4 -3
  9. package/src/lib/orchestrator/config-store.ts +22 -2
  10. package/src/lib/orchestrator/launcher.ts +52 -0
  11. package/src/lib/orchestrator/setup-compatibilities.ts +8 -0
  12. package/src/lib/orchestrator/types.ts +1 -0
  13. package/src/opencode/companion-hooks.ts +1 -1
  14. package/src/plugins.ts +2 -0
  15. package/src/templates/capabilities/openspec-cap/claude/hooks/mate-artifact-finish.sh +160 -0
  16. package/src/templates/capabilities/openspec-cap/mate-skills/{mate-openspec-artifact-finish → mate-artifact-finish}/SKILL.md +1 -1
  17. package/src/templates/capabilities/openspec-cap/mate-skills/{mate-openspec-artifact-finish → mate-artifact-finish}/references/openspec.md +1 -1
  18. package/src/tools/setup/capabilities/context-mode.ts +197 -0
  19. package/src/tools/setup/capabilities/graphify.ts +1 -1
  20. package/src/tools/setup/capabilities/headroom.ts +3 -98
  21. package/src/tools/setup/capabilities/openspec.ts +20 -7
  22. package/src/tools/setup/capabilities/rtk.ts +107 -0
  23. package/src/tools/setup/context-services.ts +6 -2
  24. package/src/tools/setup/plugin.ts +10 -2
  25. package/src/tools/setup/providers/claude.ts +4 -18
  26. package/src/tools/setup.ts +1 -1
  27. package/src/templates/capabilities/openspec-cap/claude/hooks/mate-openspec-artifact-finish.sh +0 -236
@@ -1,236 +0,0 @@
1
- #!/bin/sh
2
- set -u
3
-
4
- input_file=$(mktemp "${TMPDIR:-/tmp}/mate-openspec-artifact-finish.XXXXXX")
5
- trap 'rm -f "$input_file"' EXIT
6
- cat >"$input_file"
7
-
8
- script_dir=$(CDPATH= cd "$(dirname "$0")" && pwd)
9
- companion_path=${MATE_ARTIFACT_PATH:-}
10
- if [ -z "$companion_path" ]; then
11
- companion_path=$(CDPATH= cd "$script_dir/../.." && pwd)
12
- fi
13
-
14
- archive_dir="$companion_path/openspec/changes/archive"
15
- state_dir="$companion_path/.claude/state"
16
-
17
- node -e '
18
- const fs = require("node:fs");
19
- const path = require("node:path");
20
-
21
- const archiveDir = process.argv[1];
22
- const stateDir = process.argv[2];
23
- const inputFile = process.argv[3];
24
- const archiveEntryPattern = /^\d{4}-\d{2}-\d{2}-.+$/;
25
- const archivePathPattern = /(?:^|[\s\/"\x27`,(])openspec\/changes\/archive\/(\d{4}-\d{2}-\d{2}-[^\/\s"\x27`,)]+)/;
26
-
27
- function readArchiveEntries() {
28
- try {
29
- return fs.readdirSync(archiveDir, { withFileTypes: true })
30
- .filter((entry) => entry.isDirectory() && archiveEntryPattern.test(entry.name))
31
- .map((entry) => entry.name)
32
- .sort();
33
- } catch {
34
- return [];
35
- }
36
- }
37
-
38
- function shellSplit(command) {
39
- const parts = [];
40
- const singleQuote = String.fromCharCode(39);
41
- let current = "";
42
- let quote = null;
43
- let escaping = false;
44
- for (let index = 0; index < command.length; index += 1) {
45
- const char = command[index];
46
- if (escaping) {
47
- current += char;
48
- escaping = false;
49
- continue;
50
- }
51
- if (char === "\\" && quote !== singleQuote) {
52
- const next = command[index + 1] || "";
53
- if (next === "\\" || /\s/.test(next) || next === singleQuote || next === String.fromCharCode(34)) {
54
- escaping = true;
55
- continue;
56
- }
57
- }
58
- if ((char === singleQuote || char === String.fromCharCode(34)) && !quote) {
59
- quote = char;
60
- continue;
61
- }
62
- if (quote === char) {
63
- quote = null;
64
- continue;
65
- }
66
- if (!quote && /\s/.test(char)) {
67
- if (current) parts.push(current);
68
- current = "";
69
- continue;
70
- }
71
- current += char;
72
- }
73
- if (current) parts.push(current);
74
- return parts;
75
- }
76
-
77
- function extractChangeFromPath(value) {
78
- const match = value.replace(/\\/g, "/").match(archivePathPattern);
79
- return match ? match[1].replace(/^\d{4}-\d{2}-\d{2}-/, "") : null;
80
- }
81
-
82
- function extractArchiveCommand(command) {
83
- const parts = shellSplit(command);
84
- const index = parts.findIndex((part, position) =>
85
- part === "archive" && position > 0 &&
86
- (parts[position - 1] === "openspec" || parts[position - 1].endsWith("/openspec")));
87
- if (index < 0) return null;
88
- for (let position = index + 1; position < parts.length; position += 1) {
89
- const part = parts[position];
90
- if (!part || part === "--") continue;
91
- if (part.startsWith("-")) {
92
- if (part === "--store") position += 1;
93
- continue;
94
- }
95
- return part;
96
- }
97
- return null;
98
- }
99
-
100
- function extractMoveCommand(command) {
101
- const parts = shellSplit(command);
102
- const moveCommands = new Set([
103
- "mv", "move", "move-item", "cmd", "cmd.exe", "powershell", "powershell.exe", "pwsh", "pwsh.exe",
104
- ]);
105
- const isPython = (part) => /^python(?:\d(?:\.\d+)*)?(?:\.exe)?$/i.test(part.split(/[\\/]/).pop() || part);
106
- for (let index = 0; index < parts.length; index += 1) {
107
- if (moveCommands.has(parts[index].toLowerCase())) {
108
- for (const part of parts.slice(index + 1)) {
109
- const change = extractChangeFromPath(part);
110
- if (change) return change;
111
- }
112
- }
113
- if (isPython(parts[index]) && /(?:shutil\.move|os\.rename|\.rename\s*\()/.test(command)) {
114
- return extractChangeFromPath(command);
115
- }
116
- }
117
- return null;
118
- }
119
-
120
- function extractFinishCommand(command) {
121
- const parts = shellSplit(command);
122
- return parts.some((part, position) =>
123
- part === "finish" && position > 1 && parts[position - 1] === "artifact" &&
124
- (parts[position - 2] === "mate" || parts[position - 2].endsWith("/mate")));
125
- }
126
-
127
- function commandSucceeded(value) {
128
- if (!value || typeof value !== "object") return true;
129
- const candidates = [value, value.result, value.output, value.tool_output, value.tool_response];
130
- for (const candidate of candidates) {
131
- if (!candidate || typeof candidate !== "object") continue;
132
- if (typeof candidate.success === "boolean") return candidate.success;
133
- for (const key of ["exitCode", "exit_code", "status", "statusCode", "status_code"]) {
134
- const status = candidate[key];
135
- if (typeof status === "number") return status === 0;
136
- if (typeof status === "string" && status.trim()) return Number(status) === 0;
137
- }
138
- }
139
- return true;
140
- }
141
-
142
- function bashCommands(input) {
143
- if (!input || typeof input !== "object") return [];
144
- const toolName = String(input.tool_name || input.toolName || input.tool || "");
145
- if (toolName !== "Bash") return [];
146
- return [
147
- input.command,
148
- input.args?.command,
149
- input.tool_input?.command,
150
- ].filter((command) => typeof command === "string");
151
- }
152
-
153
- function commandArchive(input) {
154
- if (!commandSucceeded(input)) return null;
155
- for (const command of bashCommands(input)) {
156
- const change = extractArchiveCommand(command) || extractMoveCommand(command);
157
- if (change) return change;
158
- }
159
- return null;
160
- }
161
-
162
- // `mate artifact finish` archives as part of its own pipeline; new archive entries it
163
- // creates are already being finished and must not trigger a nudge (its JSON result
164
- // already tells the agent how to resume after a failure).
165
- function commandFinish(input) {
166
- return bashCommands(input).some((command) => extractFinishCommand(command));
167
- }
168
-
169
- let input = {};
170
- try {
171
- input = JSON.parse(fs.readFileSync(inputFile, "utf8") || "{}");
172
- } catch {}
173
-
174
- const sessionId = typeof input.session_id === "string" && input.session_id ? input.session_id : null;
175
- const stateFile = sessionId
176
- ? path.join(stateDir, "mate-openspec-artifact-finish." + sessionId.replace(/[^a-zA-Z0-9_-]/g, "_") + ".json")
177
- : path.join(stateDir, "mate-openspec-artifact-finish.archive-snapshot.json");
178
-
179
- const currentEntries = readArchiveEntries();
180
- let previousEntries;
181
- let validState = false;
182
- try {
183
- const state = JSON.parse(fs.readFileSync(stateFile, "utf8"));
184
- if (state && state.version === 1 && Array.isArray(state.entries) &&
185
- state.entries.every((entry) => typeof entry === "string" && archiveEntryPattern.test(entry))) {
186
- previousEntries = new Set(state.entries);
187
- validState = true;
188
- }
189
- } catch {}
190
-
191
- try {
192
- fs.mkdirSync(path.dirname(stateFile), { recursive: true });
193
- fs.writeFileSync(stateFile, JSON.stringify({ version: 1, entries: currentEntries }) + "\n");
194
- } catch {
195
- process.exit(0);
196
- }
197
-
198
- function nudgeFor(change) {
199
- return (
200
- "An openspec change (" + change + ") was just archived but not finished. Invoke the " +
201
- "mate-openspec-artifact-finish skill now to finish it (invoke the CLI as `mate`, never through a companion-local wrapper path or as `$MATE_COMPANION_BIN_PATH/mate`). " +
202
- "`mate artifact finish` resumes from the already-archived change and completes the commit, dated tag, and push itself — " +
203
- "do not hand-commit, do not hand-tag, and do not re-apply delta specs manually. " +
204
- "Follow the skill guardrails, including confirming with the user in chat before pushing."
205
- );
206
- }
207
-
208
- const eventName = typeof input.hook_event_name === "string" && input.hook_event_name
209
- ? input.hook_event_name
210
- : "PostToolUse";
211
-
212
- function emit(message) {
213
- if (eventName === "Stop") {
214
- // Stop hooks deliver instructions via a block decision, not additionalContext.
215
- process.stdout.write(JSON.stringify({ decision: "block", reason: message }));
216
- } else {
217
- process.stdout.write(JSON.stringify({
218
- hookSpecificOutput: { hookEventName: eventName, additionalContext: message },
219
- }));
220
- }
221
- process.exit(0);
222
- }
223
-
224
- // A finish run archives and finishes in one pipeline: absorb its entries silently.
225
- if (commandFinish(input)) process.exit(0);
226
-
227
- const commandChange = commandArchive(input);
228
- if (commandChange) emit(nudgeFor(commandChange));
229
-
230
- if (!validState) process.exit(0);
231
- const newlyArchived = currentEntries.filter((entry) => !previousEntries.has(entry));
232
- if (newlyArchived.length === 0) process.exit(0);
233
-
234
- emit(newlyArchived.map((entry) => nudgeFor(entry.slice("YYYY-MM-DD-".length))).join("\n\n"));
235
- ' "$archive_dir" "$state_dir" "$input_file"
236
- exit 0