@uniqbit/mate-core 0.15.4-canary.8 → 0.15.4

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 (44) hide show
  1. package/claude-plugin/hooks/artifact-finish-nudge.mjs +5 -3
  2. package/claude-plugin/hooks/session-banner.mjs +5 -3
  3. package/claude-plugin/hooks/ts-loader.mjs +28 -0
  4. package/claude-plugin/hooks/validate-artifact-path.mjs +5 -3
  5. package/package.json +1 -1
  6. package/src/cli/commands/cap/openspec.ts +20 -1
  7. package/src/cli/commands/companion/hub.ts +1 -0
  8. package/src/cli/commands/plugin/install.ts +15 -3
  9. package/src/hooks/artifact-finish-nudge.ts +35 -3
  10. package/src/lib/context-mode-package.ts +5 -3
  11. package/src/lib/orchestrator/adapters/opencode.ts +10 -60
  12. package/src/lib/orchestrator/companion-git-sync.ts +67 -8
  13. package/src/lib/orchestrator/companion-hub.ts +28 -6
  14. package/src/lib/orchestrator/types.ts +2 -2
  15. package/src/lib/package-paths.ts +1 -0
  16. package/src/opencode/companion-hooks.ts +33 -12
  17. package/src/playbooks/companion-guidance.ts +1 -1
  18. package/src/templates/capabilities/openspec-cap/mate-skills/agents/mate-openspec-backfill/SKILL.md +65 -0
  19. package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +1 -0
  20. package/src/templates/root/TEMPLATE_AGENTS.md +2 -0
  21. package/src/templates/root/TEMPLATE_CLAUDE.md +2 -0
  22. package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +3612 -0
  23. package/src/tools/setup/capabilities/context-mode.ts +57 -83
  24. package/src/tools/setup/capabilities/graphify-shared.ts +27 -0
  25. package/src/tools/setup/capabilities/graphify.ts +86 -295
  26. package/src/tools/setup/capabilities/openspec.ts +34 -1
  27. package/src/tools/setup/capabilities/react-doctor.ts +37 -37
  28. package/src/tools/setup/capabilities/rtk.ts +4 -1
  29. package/src/tools/setup/capabilities/tokensave-shared.ts +6 -0
  30. package/src/tools/setup/capabilities/tokensave.ts +51 -72
  31. package/src/tools/setup/context-services.ts +29 -0
  32. package/src/tools/setup/dynamic-plugins/hydrate.ts +15 -1
  33. package/src/tools/setup/engine.ts +68 -1
  34. package/src/tools/setup/mate.ts +10 -2
  35. package/src/tools/setup/plugin.ts +105 -0
  36. package/src/tools/setup/plugins/gitignore.ts +7 -4
  37. package/src/tools/setup/plugins/guidance.ts +1 -1
  38. package/src/tools/setup/providers/agent-file-sections.ts +78 -0
  39. package/src/tools/setup/providers/claude-format.ts +159 -0
  40. package/src/tools/setup/providers/claude.ts +281 -227
  41. package/src/tools/setup/providers/opencode-format.ts +146 -0
  42. package/src/tools/setup/providers/opencode.ts +209 -60
  43. package/src/tools/setup/providers/skill-tree.ts +38 -0
  44. package/src/tools/setup.ts +7 -56
@@ -0,0 +1,159 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ // Structurally matches `McpServerDescriptor` from ../plugin; declared here so
5
+ // the format module stays dependency-free of the plugin contract (which
6
+ // imports hook types from this module).
7
+ export interface McpEntryDescriptor {
8
+ name: string;
9
+ command?: string;
10
+ args?: string[];
11
+ url?: string;
12
+ env?: Record<string, string>;
13
+ }
14
+
15
+ // Claude runtime config format primitives: parse/merge/serialize for
16
+ // `settings.local.json` (hook maps, permissions) and `.mcp.json`. This module
17
+ // owns the file formats only — which entries are Mate-managed is the callers'
18
+ // (Runtime Surface) knowledge.
19
+
20
+ export interface ClaudeHookCommand {
21
+ type?: string;
22
+ command?: string;
23
+ args?: string[];
24
+ timeout?: number;
25
+ }
26
+
27
+ export interface ClaudeHookGroup {
28
+ matcher?: string;
29
+ hooks?: ClaudeHookCommand[];
30
+ }
31
+
32
+ export type ClaudeHookMap = Record<string, ClaudeHookGroup[]>;
33
+
34
+ export interface ClaudeSettings {
35
+ hooks?: ClaudeHookMap;
36
+ permissions?: { additionalDirectories?: string[]; allow?: string[] } & Record<string, unknown>;
37
+ mcpServers?: Record<string, { command?: string; args?: string[] }>;
38
+ [key: string]: unknown;
39
+ }
40
+
41
+ export interface ClaudeMcpConfig {
42
+ mcpServers?: Record<string, unknown>;
43
+ [key: string]: unknown;
44
+ }
45
+
46
+ function hookGroupKey(group: ClaudeHookGroup): string {
47
+ return JSON.stringify(group);
48
+ }
49
+
50
+ /** Append incoming hook groups per event, skipping structurally identical ones. */
51
+ export function mergeClaudeHookGroups(
52
+ existingHooks: ClaudeHookMap,
53
+ incomingHooks: ClaudeHookMap,
54
+ ): ClaudeHookMap {
55
+ const merged: ClaudeHookMap = { ...existingHooks };
56
+ for (const [event, groups] of Object.entries(incomingHooks)) {
57
+ const current = merged[event] ?? [];
58
+ const seen = new Set(current.map(hookGroupKey));
59
+ merged[event] = [...current];
60
+ for (const group of groups ?? []) {
61
+ const key = hookGroupKey(group);
62
+ if (seen.has(key)) continue;
63
+ merged[event].push(group);
64
+ seen.add(key);
65
+ }
66
+ }
67
+ return merged;
68
+ }
69
+
70
+ /** Keep only groups matching `keep`; events left empty are dropped. */
71
+ export function filterClaudeHookGroups(
72
+ hooks: ClaudeHookMap,
73
+ keep: (group: ClaudeHookGroup) => boolean,
74
+ ): ClaudeHookMap {
75
+ const filtered: ClaudeHookMap = {};
76
+ for (const [event, groups] of Object.entries(hooks)) {
77
+ const remaining = (groups ?? []).filter(keep);
78
+ if (remaining.length > 0) {
79
+ filtered[event] = remaining;
80
+ }
81
+ }
82
+ return filtered;
83
+ }
84
+
85
+ /** Tolerant read: absent or malformed settings read as an empty object. */
86
+ export async function readClaudeSettings(settingsPath: string): Promise<ClaudeSettings> {
87
+ try {
88
+ const parsed = JSON.parse(await fs.readFile(settingsPath, "utf8")) as unknown;
89
+ if (parsed && typeof parsed === "object") {
90
+ return parsed as ClaudeSettings;
91
+ }
92
+ } catch {
93
+ // Absent or unparseable — start from an empty object.
94
+ }
95
+ return {};
96
+ }
97
+
98
+ export async function writeClaudeSettings(
99
+ settingsPath: string,
100
+ settings: ClaudeSettings,
101
+ ): Promise<void> {
102
+ await fs.mkdir(path.dirname(settingsPath), { recursive: true });
103
+ await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
104
+ }
105
+
106
+ /** Tolerant read of `.mcp.json`; `present` distinguishes absent from empty. */
107
+ export async function readClaudeMcpConfig(
108
+ mcpConfigPath: string,
109
+ ): Promise<{ present: boolean; config: ClaudeMcpConfig }> {
110
+ try {
111
+ const parsed = JSON.parse(await fs.readFile(mcpConfigPath, "utf8")) as unknown;
112
+ if (parsed && typeof parsed === "object") {
113
+ return { present: true, config: parsed as ClaudeMcpConfig };
114
+ }
115
+ } catch {
116
+ // Absent or unparseable — start from an empty object.
117
+ }
118
+ return { present: false, config: {} };
119
+ }
120
+
121
+ export async function writeClaudeMcpConfig(
122
+ mcpConfigPath: string,
123
+ config: ClaudeMcpConfig,
124
+ ): Promise<void> {
125
+ await fs.writeFile(mcpConfigPath, JSON.stringify(config, null, 2) + "\n", "utf8");
126
+ }
127
+
128
+ /** Map a provider-agnostic MCP descriptor to Claude's `.mcp.json` entry shape. */
129
+ export function toClaudeMcpEntry(descriptor: McpEntryDescriptor): Record<string, unknown> {
130
+ if (descriptor.url) return { url: descriptor.url };
131
+ return {
132
+ command: descriptor.command,
133
+ ...(descriptor.args ? { args: descriptor.args } : {}),
134
+ ...(descriptor.env ? { env: descriptor.env } : {}),
135
+ };
136
+ }
137
+
138
+ /**
139
+ * Reconcile a single server entry in `.mcp.json` while preserving every
140
+ * unrelated key. `entry: null` removes the server; removal never creates the
141
+ * file.
142
+ */
143
+ export async function updateClaudeMcpServer(
144
+ mcpConfigPath: string,
145
+ name: string,
146
+ entry: Record<string, unknown> | null,
147
+ ): Promise<void> {
148
+ const { present, config } = await readClaudeMcpConfig(mcpConfigPath);
149
+ if (entry === null && !present) return;
150
+
151
+ const mcpServers: Record<string, unknown> = { ...config.mcpServers };
152
+ if (entry === null) {
153
+ if (!(name in mcpServers)) return;
154
+ delete mcpServers[name];
155
+ } else {
156
+ mcpServers[name] = entry;
157
+ }
158
+ await writeClaudeMcpConfig(mcpConfigPath, { ...config, mcpServers });
159
+ }