@uniqbit/mate-core 0.18.4 → 1.0.0-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/claude-plugin/hooks/companion-instructions.mjs +9 -0
- package/claude-plugin/hooks/hooks.json +12 -2
- 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 +10 -2
- package/src/brand/art.generated.ts +69 -0
- package/src/brand/index.ts +12 -0
- package/src/brand/palette.ts +19 -0
- package/src/brand/svg.generated.ts +8 -0
- package/src/brand/terminal.ts +83 -0
- package/src/brand/types.ts +33 -0
- package/src/cli/commands/artifact/artifact.ts +8 -0
- package/src/cli/commands/artifact/delivery/archive.ts +117 -0
- package/src/cli/commands/artifact/delivery/branch-flags.ts +55 -0
- package/src/cli/commands/artifact/delivery/environment.ts +104 -0
- package/src/cli/commands/artifact/delivery/link.ts +154 -0
- package/src/cli/commands/artifact/delivery/output.ts +58 -0
- package/src/cli/commands/artifact/finish/command.ts +33 -3
- package/src/cli/commands/artifact/finish/engine.ts +47 -3
- package/src/cli/commands/artifact/finish/finisher.ts +10 -0
- package/src/cli/commands/artifact/finish/openspec.ts +200 -129
- package/src/cli/commands/artifact/finish/secret-scan.ts +43 -0
- package/src/cli/commands/artifact/pending/command.ts +31 -3
- package/src/cli/commands/artifact/pending/discovery.ts +21 -57
- package/src/cli/commands/cap/graphify.ts +52 -3
- package/src/cli/commands/cap/index-cmd.ts +8 -1
- package/src/cli/commands/cap/openspec.ts +13 -25
- package/src/cli/commands/companion/open.ts +56 -39
- package/src/cli/commands/companion/update.ts +75 -42
- package/src/cli/commands/doctor.ts +257 -78
- package/src/cli/commands/install.ts +0 -8
- package/src/cli/commands/plugin/install.ts +9 -4
- package/src/cli/commands/plugin/plugin.ts +3 -2
- package/src/cli/commands/report/adapter.ts +11 -2
- package/src/cli/commands/report/contract.ts +21 -0
- package/src/cli/commands/report/highlight.ts +34 -4
- package/src/cli/commands/report/markdown.ts +110 -0
- package/src/cli/commands/report/palette.ts +118 -0
- package/src/cli/commands/report/renderer.ts +456 -153
- package/src/cli/commands/report/types.ts +16 -0
- package/src/cli/commands/setup.ts +7 -15
- package/src/cli/commands/studio/client-asset.ts +16 -0
- package/src/cli/commands/studio/openspec-cli.ts +10 -58
- package/src/cli/commands/studio/routes.ts +2 -0
- package/src/cli/commands/studio/selection.ts +19 -1
- package/src/cli/commands/studio/server.ts +42 -1
- package/src/cli/commands/studio/terminal-launch.ts +7 -16
- package/src/cli/commands/studio/views/client.ts +6 -616
- package/src/cli/commands/studio/views/dashboard/changes.tsx +5 -10
- package/src/cli/commands/studio/views/document.tsx +137 -69
- package/src/cli/commands/studio/views/markdown.tsx +250 -0
- package/src/cli/commands/studio/views/palette.ts +134 -0
- package/src/cli/commands/studio/views/reports/index.tsx +2 -7
- package/src/cli/commands/studio/views/skills/index.tsx +43 -62
- package/src/cli/commands/studio/views/specs/index.tsx +7 -15
- package/src/cli/commands/studio/views/studio-client/actions.ts +51 -0
- package/src/cli/commands/studio/views/studio-client/companion.ts +13 -0
- package/src/cli/commands/studio/views/studio-client/index.ts +32 -0
- package/src/cli/commands/studio/views/studio-client/keys.ts +3 -0
- package/src/cli/commands/studio/views/studio-client/main.ts +3 -0
- package/src/cli/commands/studio/views/studio-client/page.ts +146 -0
- package/src/cli/commands/studio/views/studio-client/panels.ts +57 -0
- package/src/cli/commands/studio/views/studio-client/reports.ts +122 -0
- package/src/cli/commands/studio/views/studio-client/theme.ts +63 -0
- package/src/cli/commands/studio/views/studio-client/vault-editor.ts +166 -0
- package/src/cli/commands/studio/views/studio-client/vault-pane.ts +202 -0
- package/src/cli/commands/studio/views/studio-client/vault-tree.ts +258 -0
- package/src/cli/commands/studio/views/studio-client.generated.ts +2 -0
- package/src/cli/commands/studio/views/styles.ts +217 -206
- package/src/cli/commands/studio/views/vault/state.ts +3 -5
- package/src/cli/commands/studio/views/workflow/delivery-path.tsx +370 -0
- package/src/cli/commands/studio/views/workflow/graph.ts +220 -0
- package/src/cli/commands/studio/views/workflow/index.tsx +2 -3
- package/src/cli/commands/studio/views/workflow/steps.ts +228 -287
- package/src/cli/commands/wrap.ts +9 -42
- package/src/cli/main.ts +4 -1
- package/src/cli/parse-flags.ts +25 -7
- package/src/cli/setup-selector.tsx +38 -162
- package/src/cli/usage.ts +5 -2
- package/src/hooks/companion-instructions.ts +190 -0
- package/src/hooks/hook-io.ts +63 -0
- package/src/hooks/session-banner.ts +21 -29
- package/src/hooks/session-guidance.ts +10 -36
- package/src/hooks/session-prepare.ts +115 -28
- package/src/hooks/validate-artifact-path.ts +82 -24
- package/src/index.ts +4 -0
- package/src/lib/change-layout.ts +154 -0
- package/src/lib/components/wizard-header.tsx +3 -1
- package/src/lib/context-mode-package.ts +24 -11
- package/src/lib/delivery/archive-change.ts +154 -0
- package/src/lib/delivery/branches.ts +108 -0
- package/src/lib/delivery/environment.ts +29 -0
- package/src/lib/delivery/forge.ts +279 -0
- package/src/lib/delivery/overlap.ts +202 -0
- package/src/lib/delivery/record-delivery.ts +196 -0
- package/src/lib/delivery/record.ts +343 -0
- package/src/lib/delivery/refresh.ts +174 -0
- package/src/lib/delivery/repository-access.ts +97 -0
- package/src/lib/delivery/stages.ts +111 -0
- package/src/lib/delivery/token.ts +58 -0
- package/src/lib/frontmatter.ts +18 -0
- package/src/lib/opencode-plugin-package.ts +5 -5
- package/src/lib/openspec-cli.ts +116 -0
- package/src/lib/openspec-scopes.ts +86 -0
- package/src/lib/orchestrator/adapters/claude-args.ts +50 -0
- package/src/lib/orchestrator/adapters/claude.ts +60 -162
- package/src/lib/orchestrator/adapters/opencode.ts +9 -24
- package/src/lib/orchestrator/config-store.ts +2 -70
- package/src/lib/orchestrator/editor.ts +9 -9
- package/src/lib/orchestrator/openspec-store.ts +14 -54
- package/src/lib/orchestrator/projection-claude-commands.ts +36 -0
- package/src/lib/orchestrator/projection-claude-links.ts +159 -0
- package/src/lib/orchestrator/projection-claude-skills.ts +19 -190
- package/src/lib/orchestrator/projection-entries.ts +45 -15
- package/src/lib/orchestrator/projection-runtime-documents.ts +15 -6
- package/src/lib/orchestrator/projection-types.ts +3 -2
- package/src/lib/orchestrator/refresh-working-claude-links.ts +87 -0
- package/src/lib/orchestrator/session-prepare.ts +49 -87
- package/src/lib/orchestrator/setup-compatibilities.ts +18 -75
- package/src/lib/orchestrator/types.ts +2 -2
- package/src/lib/orchestrator/wrap-pass.ts +22 -88
- package/src/lib/package-paths.ts +8 -0
- package/src/opencode/companion-hooks.ts +64 -5
- package/src/opencode/companion-policy.ts +37 -18
- package/src/opencode/projected-guidance.ts +4 -1
- package/src/opencode/session-prepare.ts +2 -1
- package/src/opencode/tui.tsx +4 -2
- package/src/runtime/artifact-refusal.ts +63 -0
- package/src/runtime/claude-delivery-plan.ts +89 -7
- package/src/runtime/commit-attribution-guard.ts +181 -0
- package/src/runtime/companion-guidance.ts +15 -3
- package/src/runtime/companion-instructions.ts +340 -0
- package/src/runtime/error-message.ts +4 -0
- package/src/runtime/guidance.ts +0 -17
- package/src/runtime/index.ts +5 -0
- package/src/runtime/managed-block.ts +117 -0
- package/src/runtime/markdown-scan.ts +266 -0
- package/src/runtime/policy.ts +16 -15
- package/src/runtime/projected-flag.ts +7 -0
- package/src/runtime/projected-guidance.ts +1 -0
- package/src/runtime/runtime-document-manifest.ts +62 -28
- package/src/runtime/secret-guard.ts +148 -0
- package/src/runtime/secret-patterns.ts +164 -0
- package/src/runtime/secret-scan.ts +90 -0
- package/src/runtime/session-kind.ts +13 -0
- package/src/runtime/shell-heredoc.ts +81 -0
- package/src/runtime/wrap-state.ts +36 -6
- package/src/runtime/write-atomic.ts +64 -0
- package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +8 -6
- package/src/templates/capabilities/openspec-cap/mate-minimal/templates/spec.md +3 -5
- package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +7 -9
- package/src/templates/capabilities/openspec-cap/mate-v1/templates/spec.md +3 -5
- package/src/templates/capabilities/openspec-cap/openspec-conventions.yaml +10 -12
- package/src/templates/mate-skills/THIRD-PARTY-NOTICES.md +40 -0
- package/src/templates/mate-skills/agents/mate-apply-change/SKILL.md +76 -0
- package/src/templates/mate-skills/agents/mate-apply-change/references/openspec-apply-change.md +198 -0
- package/src/templates/mate-skills/agents/mate-archive-change/SKILL.md +59 -0
- package/src/templates/mate-skills/{claude/mate-openspec-backfill → agents/mate-backfill-spec}/SKILL.md +4 -4
- package/src/templates/mate-skills/agents/mate-clarify-intent/SKILL.md +95 -0
- package/src/templates/mate-skills/agents/mate-explore-change/SKILL.md +50 -0
- package/src/templates/mate-skills/agents/mate-explore-change/references/openspec-explore.md +348 -0
- package/src/templates/mate-skills/agents/mate-grill-me/SKILL.md +3 -3
- package/src/templates/mate-skills/agents/mate-interview-me/SKILL.md +5 -5
- package/src/templates/mate-skills/agents/mate-propose-change/SKILL.md +103 -0
- package/src/templates/mate-skills/agents/mate-propose-change/references/openspec-propose.md +167 -0
- package/src/templates/mate-skills/agents/mate-propose-change/references/openspec-update-change.md +101 -0
- package/src/templates/mate-skills/agents/{mate-artifact-publish → mate-publish-artifact}/SKILL.md +91 -23
- package/src/templates/mate-skills/agents/{mate-artifact-publish → mate-publish-artifact}/references/openspec.md +4 -3
- package/src/templates/mate-skills/agents/mate-show-me/SKILL.md +74 -16
- package/src/templates/mate-skills/agents/mate-simplify-code/SKILL.md +74 -68
- package/src/templates/mate-skills/claude/mate-apply-change/SKILL.md +76 -0
- package/src/templates/mate-skills/claude/mate-apply-change/references/openspec-apply-change.md +198 -0
- package/src/templates/mate-skills/claude/mate-archive-change/SKILL.md +59 -0
- package/src/templates/mate-skills/{agents/mate-openspec-backfill → claude/mate-backfill-spec}/SKILL.md +4 -4
- package/src/templates/mate-skills/claude/mate-clarify-intent/SKILL.md +95 -0
- package/src/templates/mate-skills/claude/mate-explore-change/SKILL.md +50 -0
- package/src/templates/mate-skills/claude/mate-explore-change/references/openspec-explore.md +348 -0
- package/src/templates/mate-skills/claude/mate-grill-me/SKILL.md +3 -3
- package/src/templates/mate-skills/claude/mate-interview-me/SKILL.md +5 -5
- package/src/templates/mate-skills/claude/mate-propose-change/SKILL.md +103 -0
- package/src/templates/mate-skills/claude/mate-propose-change/references/openspec-propose.md +167 -0
- package/src/templates/mate-skills/claude/mate-propose-change/references/openspec-update-change.md +101 -0
- package/src/templates/mate-skills/claude/{mate-artifact-publish → mate-publish-artifact}/SKILL.md +91 -23
- package/src/templates/mate-skills/claude/{mate-artifact-publish → mate-publish-artifact}/references/openspec.md +4 -3
- package/src/templates/mate-skills/claude/mate-show-me/SKILL.md +74 -16
- package/src/templates/mate-skills/claude/mate-simplify-code/SKILL.md +74 -68
- package/src/templates/mate-skills/openspec-references.ts +140 -0
- package/src/templates/root/TEMPLATE_AGENTS.md +2 -2
- package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +3727 -320
- package/src/tools/setup/capabilities/context-mode.ts +25 -5
- package/src/tools/setup/capabilities/graphify-gate.ts +31 -0
- package/src/tools/setup/capabilities/graphify-secrets.ts +255 -0
- package/src/tools/setup/capabilities/graphify.ts +12 -0
- package/src/tools/setup/capabilities/openspec.ts +273 -270
- package/src/tools/setup/capabilities/tokensave.ts +36 -3
- package/src/tools/setup/context-services.ts +9 -17
- 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/dynamic-plugins/install.ts +24 -28
- package/src/tools/setup/engine.ts +93 -148
- package/src/tools/setup/managed-output.ts +51 -0
- package/src/tools/setup/mate.ts +86 -11
- package/src/tools/setup/plugin.ts +37 -17
- package/src/tools/setup/plugins/gitignore.ts +1 -7
- package/src/tools/setup/plugins/guidance.ts +7 -4
- package/src/tools/setup/providers/agent-file-sections.ts +4 -2
- package/src/tools/setup/providers/claude-attribution.ts +65 -0
- package/src/tools/setup/providers/claude-plugin-folders.ts +625 -0
- package/src/tools/setup/providers/claude-plugin-hooks.ts +39 -19
- package/src/tools/setup/providers/claude.ts +285 -34
- package/src/tools/setup/providers/opencode.ts +180 -11
- package/src/tools/setup/registry.ts +2 -24
- package/src/tools/setup/surface-target.ts +4 -13
- package/src/tools/setup/utils.ts +14 -4
- package/src/tools/setup.ts +131 -18
- package/wrappers/bin/graphify +52 -4
- package/wrappers/bin/openspec +33 -9
- package/wrappers/lib/graphify-gate.mjs +8 -0
- package/opencode-plugin/src/audience-layer.ts +0 -91
- package/src/cli/commands/studio/views/workflow/transcript.tsx +0 -190
- 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/runtime-documents.ts +0 -291
- package/src/tools/setup/upgrade-sweep.ts +0 -153
|
@@ -6,12 +6,14 @@ import {
|
|
|
6
6
|
CONTEXT_MODE_VERSION,
|
|
7
7
|
getContextModeInstallDir,
|
|
8
8
|
getContextModePackageReference,
|
|
9
|
+
getContextModePackageRoot,
|
|
9
10
|
installContextModePackage,
|
|
10
11
|
isContextModePackageReference,
|
|
11
12
|
validateContextModePackage,
|
|
12
13
|
} from "../../../lib/context-mode-package";
|
|
13
14
|
import { warmOpenCodePackageCache } from "../../../lib/opencode-plugin-package";
|
|
14
15
|
import { installedVersionAt, isPreinstalledPluginPath } from "../../../lib/preinstalled-plugins";
|
|
16
|
+
import { launchPassesClaudePluginFolders } from "../../../runtime/claude-delivery-plan";
|
|
15
17
|
import type {
|
|
16
18
|
CapabilityPlugin,
|
|
17
19
|
LaunchPreflightContext,
|
|
@@ -108,6 +110,22 @@ async function validateOpenCodeReferences(ctx: LaunchPreflightContext): Promise<
|
|
|
108
110
|
];
|
|
109
111
|
}
|
|
110
112
|
|
|
113
|
+
/** Checked only where the launch passes plugin folders, so a plain session skips it. Observational: reads files and runs `node --version`. */
|
|
114
|
+
async function validateClaudePackage(
|
|
115
|
+
ctx: LaunchPreflightContext,
|
|
116
|
+
validatePackage: typeof validateContextModePackage,
|
|
117
|
+
): Promise<string[]> {
|
|
118
|
+
if (!launchPassesClaudePluginFolders(ctx.sessionKind)) return [];
|
|
119
|
+
try {
|
|
120
|
+
await validatePackage(ctx.companionPath);
|
|
121
|
+
return [];
|
|
122
|
+
} catch (error) {
|
|
123
|
+
return [
|
|
124
|
+
`Claude context-mode plugin is unavailable: ${(error as Error).message} Repair it with \`mate companion setup\`.`,
|
|
125
|
+
];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
111
129
|
export function createContextModePlugin(deps: ContextModePluginDeps = {}): CapabilityPlugin {
|
|
112
130
|
const installPackage = deps.installPackage ?? installContextModePackage;
|
|
113
131
|
const validatePackage = deps.validatePackage ?? validateContextModePackage;
|
|
@@ -120,11 +138,11 @@ export function createContextModePlugin(deps: ContextModePluginDeps = {}): Capab
|
|
|
120
138
|
defaultSelected: false,
|
|
121
139
|
isEnabled: (config) =>
|
|
122
140
|
(config.capabilities ?? []).some((capability) => capability.name === "context-mode"),
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
getRuntimeContributions(): RuntimeContributionsByRuntime {
|
|
141
|
+
/**
|
|
142
|
+
* Every context-mode reference is Mate-managed; the installed package also
|
|
143
|
+
* supplies Claude's launch-only plugin.
|
|
144
|
+
*/
|
|
145
|
+
getRuntimeContributions(ctx): RuntimeContributionsByRuntime {
|
|
128
146
|
return {
|
|
129
147
|
claude: {
|
|
130
148
|
// The context-mode Claude plugin exposes its skill and MCP tools
|
|
@@ -134,6 +152,7 @@ export function createContextModePlugin(deps: ContextModePluginDeps = {}): Capab
|
|
|
134
152
|
"Skill(context-mode:context-mode)",
|
|
135
153
|
"mcp__plugin_context-mode_context-mode__*",
|
|
136
154
|
],
|
|
155
|
+
pluginDirectories: [{ path: getContextModePackageRoot(ctx.companionPath) }],
|
|
137
156
|
},
|
|
138
157
|
opencode: {
|
|
139
158
|
pluginReferences: [
|
|
@@ -195,6 +214,7 @@ export function createContextModePlugin(deps: ContextModePluginDeps = {}): Capab
|
|
|
195
214
|
},
|
|
196
215
|
forProvider: {
|
|
197
216
|
claude: {
|
|
217
|
+
preflight: (ctx) => validateClaudePackage(ctx, validatePackage),
|
|
198
218
|
async apply(ctx) {
|
|
199
219
|
await assertNoMcpConflict(ctx, "claude");
|
|
200
220
|
},
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { resolveSecretPolicy } from "../../../runtime/policy";
|
|
2
|
+
import { ensureGraphifySecretExclusion, pruneGraphifySecrets } from "./graphify-secrets";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The secret gate behind the `graphify` wrapper, for extractions an agent
|
|
6
|
+
* runs without `mate cap graphify`. Never fails the run: problems are
|
|
7
|
+
* warnings on stderr and the exit code is always 0.
|
|
8
|
+
*
|
|
9
|
+
* - `pre <companion> <repo>` rewrites the `.graphifyignore` region and prints
|
|
10
|
+
* the secret patterns, one per line, for the wrapper to pass as `--exclude`.
|
|
11
|
+
* - `post <companion> <graphify-out>` prunes secrets from the output.
|
|
12
|
+
*/
|
|
13
|
+
export async function runGraphifyGate(argv: string[]): Promise<number> {
|
|
14
|
+
const [mode, companionPath, target] = argv;
|
|
15
|
+
if (!companionPath || !target) return 0;
|
|
16
|
+
const policy = resolveSecretPolicy(companionPath);
|
|
17
|
+
if (mode === "pre") {
|
|
18
|
+
try {
|
|
19
|
+
await ensureGraphifySecretExclusion(target, policy);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
process.stderr.write(`graphify: warning: ${(error as Error).message}\n`);
|
|
22
|
+
}
|
|
23
|
+
if (policy.guardrails)
|
|
24
|
+
process.stdout.write(policy.deny.map((pattern) => `${pattern}\n`).join(""));
|
|
25
|
+
} else if (mode === "post") {
|
|
26
|
+
for (const note of await pruneGraphifySecrets(target, policy)) {
|
|
27
|
+
process.stderr.write(`graphify: ${note}\n`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { isSecretFile, type SecretPolicy } from "../../../runtime/secret-patterns";
|
|
5
|
+
import { scanContent, scanFiles } from "../../../runtime/secret-scan";
|
|
6
|
+
import { resolveGitInfoExcludePath } from "../git-utils";
|
|
7
|
+
|
|
8
|
+
export const GRAPHIFY_IGNORE_FILE = ".graphifyignore";
|
|
9
|
+
const REGION_BEGIN = "# mate:secrets begin (managed by Mate; edits are overwritten)";
|
|
10
|
+
const REGION_END = "# mate:secrets end";
|
|
11
|
+
|
|
12
|
+
export class GraphifySecretExclusionError extends Error {
|
|
13
|
+
constructor(message: string) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = "GraphifySecretExclusionError";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function renderRegion(policy: SecretPolicy): string[] {
|
|
20
|
+
return [
|
|
21
|
+
REGION_BEGIN,
|
|
22
|
+
...policy.deny,
|
|
23
|
+
...policy.allow.map((pattern) => `!${pattern}`),
|
|
24
|
+
REGION_END,
|
|
25
|
+
];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Content with every Mate region removed, so a hand-copied duplicate goes too. */
|
|
29
|
+
function stripRegions(content: string): string[] {
|
|
30
|
+
const kept: string[] = [];
|
|
31
|
+
let inside = false;
|
|
32
|
+
for (const line of content.split(/\r?\n/)) {
|
|
33
|
+
if (line.startsWith("# mate:secrets begin")) inside = true;
|
|
34
|
+
else if (inside && line === REGION_END) inside = false;
|
|
35
|
+
else if (!inside) kept.push(line);
|
|
36
|
+
}
|
|
37
|
+
while (kept.length > 0 && kept.at(-1)?.trim() === "") kept.pop();
|
|
38
|
+
return kept;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The region is intact when the file ends with exactly the lines Mate renders. */
|
|
42
|
+
function endsWithRegion(content: string, region: string[]): boolean {
|
|
43
|
+
const lines = content.split(/\r?\n/);
|
|
44
|
+
while (lines.length > 0 && lines.at(-1) === "") lines.pop();
|
|
45
|
+
const tail = lines.slice(-region.length);
|
|
46
|
+
return tail.length === region.length && tail.every((line, index) => line === region[index]);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function excludeFromGit(repoPath: string): Promise<void> {
|
|
50
|
+
const excludePath = await resolveGitInfoExcludePath(repoPath);
|
|
51
|
+
if (!excludePath) return;
|
|
52
|
+
let existing = "";
|
|
53
|
+
try {
|
|
54
|
+
existing = await fs.readFile(excludePath, "utf8");
|
|
55
|
+
} catch {
|
|
56
|
+
// Fresh repos may not have created info/exclude yet.
|
|
57
|
+
}
|
|
58
|
+
const entry = `/${GRAPHIFY_IGNORE_FILE}`;
|
|
59
|
+
if (existing.split(/\r?\n/).some((line) => line.trim() === entry)) return;
|
|
60
|
+
await fs.mkdir(path.dirname(excludePath), { recursive: true });
|
|
61
|
+
const separator = existing === "" || existing.endsWith("\n") ? "" : "\n";
|
|
62
|
+
await fs.writeFile(excludePath, `${existing}${separator}${entry}\n`, "utf8");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Keeps Mate's secret region as the last block of `.graphifyignore`, which
|
|
67
|
+
* graphify reads last-match-wins, so no earlier user line can re-include a
|
|
68
|
+
* secret file. Rewritten before every Mate-run extraction, not only at setup.
|
|
69
|
+
* Throws when the region can't be written or read back; callers warn and run
|
|
70
|
+
* anyway, since the `--exclude` arguments and the output pruning still apply.
|
|
71
|
+
* With guardrails off it only removes Mate's region.
|
|
72
|
+
*/
|
|
73
|
+
export async function ensureGraphifySecretExclusion(
|
|
74
|
+
repoPath: string,
|
|
75
|
+
policy: SecretPolicy,
|
|
76
|
+
): Promise<void> {
|
|
77
|
+
const filePath = path.join(repoPath, GRAPHIFY_IGNORE_FILE);
|
|
78
|
+
let existing: string | undefined;
|
|
79
|
+
try {
|
|
80
|
+
existing = await fs.readFile(filePath, "utf8");
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
83
|
+
throw new GraphifySecretExclusionError(
|
|
84
|
+
`the secret exclusion in ${filePath} could not be read: ${(error as Error).message}`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const kept = stripRegions(existing ?? "");
|
|
90
|
+
if (!policy.guardrails) {
|
|
91
|
+
if (existing === undefined) return;
|
|
92
|
+
if (kept.every((line) => line.trim() === "")) await fs.rm(filePath, { force: true });
|
|
93
|
+
else await fs.writeFile(filePath, `${kept.join("\n")}\n`, "utf8");
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const region = renderRegion(policy);
|
|
98
|
+
const next = `${[...kept, ...(kept.length > 0 ? [""] : []), ...region].join("\n")}\n`;
|
|
99
|
+
try {
|
|
100
|
+
if (next !== existing) await fs.writeFile(filePath, next, "utf8");
|
|
101
|
+
if (!endsWithRegion(await fs.readFile(filePath, "utf8"), region)) {
|
|
102
|
+
throw new Error("the file does not end with Mate's region after writing it");
|
|
103
|
+
}
|
|
104
|
+
} catch (error) {
|
|
105
|
+
throw new GraphifySecretExclusionError(
|
|
106
|
+
`the secret exclusion in ${filePath} could not be enforced: ${(error as Error).message}`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
if (existing === undefined) await excludeFromGit(repoPath);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* `--exclude` arguments for the secret patterns. graphify applies them over
|
|
114
|
+
* every ignore file, a user's `!` negation included. Only `extract` and the
|
|
115
|
+
* `<path> --update` form accept them; the `update` subcommand rejects the flag.
|
|
116
|
+
*/
|
|
117
|
+
export function graphifySecretExcludeArgs(policy: SecretPolicy): string[] {
|
|
118
|
+
return policy.guardrails ? policy.deny.flatMap((pattern) => ["--exclude", pattern]) : [];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const GRAPH_SECTIONS = ["nodes", "links", "hyperedges"] as const;
|
|
122
|
+
|
|
123
|
+
type GraphSection = (typeof GRAPH_SECTIONS)[number];
|
|
124
|
+
|
|
125
|
+
type GraphEntry = Record<string, unknown>;
|
|
126
|
+
|
|
127
|
+
function entriesOf(graph: Record<string, unknown>, section: string): GraphEntry[] {
|
|
128
|
+
const entries = graph[section];
|
|
129
|
+
return Array.isArray(entries)
|
|
130
|
+
? entries.filter((entry): entry is GraphEntry => !!entry && typeof entry === "object")
|
|
131
|
+
: [];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Every node id an entry points at: link endpoints and hyperedge members. */
|
|
135
|
+
function referencedIds(entry: GraphEntry): string[] {
|
|
136
|
+
const ids = [entry.source, entry.target, ...(Array.isArray(entry.nodes) ? entry.nodes : [])];
|
|
137
|
+
return ids.filter((id): id is string => typeof id === "string");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Takes the secrets out of graph.json and keeps the rest: entries whose source
|
|
142
|
+
* is a secret file, entries whose content secretlint flags (one entry per line,
|
|
143
|
+
* so a finding's line names its entry), and links or hyperedges left pointing
|
|
144
|
+
* at a removed node. Returns notes naming paths and rule ids, never values.
|
|
145
|
+
*/
|
|
146
|
+
async function pruneGraph(graphPath: string, policy: SecretPolicy): Promise<string[]> {
|
|
147
|
+
let graph: Record<string, unknown>;
|
|
148
|
+
try {
|
|
149
|
+
const parsed: unknown = JSON.parse(await fs.readFile(graphPath, "utf8"));
|
|
150
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return [];
|
|
151
|
+
graph = parsed as Record<string, unknown>;
|
|
152
|
+
} catch {
|
|
153
|
+
return [];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const notes: string[] = [];
|
|
157
|
+
const dropped: Record<GraphSection, Set<number>> = {
|
|
158
|
+
nodes: new Set(),
|
|
159
|
+
links: new Set(),
|
|
160
|
+
hyperedges: new Set(),
|
|
161
|
+
};
|
|
162
|
+
const secretSources = new Set<string>();
|
|
163
|
+
const lines: { section: GraphSection; index: number }[] = [];
|
|
164
|
+
const text: string[] = [];
|
|
165
|
+
for (const section of GRAPH_SECTIONS) {
|
|
166
|
+
entriesOf(graph, section).forEach((entry, index) => {
|
|
167
|
+
const source = typeof entry.source_file === "string" ? entry.source_file : "";
|
|
168
|
+
if (source && isSecretFile(source, policy)) {
|
|
169
|
+
secretSources.add(source);
|
|
170
|
+
dropped[section].add(index);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
lines.push({ section, index });
|
|
174
|
+
text.push(JSON.stringify(entry));
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
for (const source of secretSources) notes.push(`left out secret file ${source}`);
|
|
178
|
+
|
|
179
|
+
const findings = text.length > 0 ? await scanContent(graphPath, text.join("\n")) : [];
|
|
180
|
+
const rules = new Set<string>();
|
|
181
|
+
for (const finding of findings) {
|
|
182
|
+
const line = lines[finding.line - 1];
|
|
183
|
+
if (!line) continue;
|
|
184
|
+
dropped[line.section].add(line.index);
|
|
185
|
+
rules.add(finding.ruleId);
|
|
186
|
+
}
|
|
187
|
+
const valueEntries = new Set(findings.map((finding) => finding.line)).size;
|
|
188
|
+
if (valueEntries > 0) {
|
|
189
|
+
notes.push(
|
|
190
|
+
`left out ${valueEntries} graph ${valueEntries === 1 ? "entry" : "entries"} containing a secret (${[...rules].join(", ")})`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
if (Object.values(dropped).every((indexes) => indexes.size === 0)) return notes;
|
|
194
|
+
|
|
195
|
+
const nodes = entriesOf(graph, "nodes");
|
|
196
|
+
const removedIds = new Set(
|
|
197
|
+
nodes
|
|
198
|
+
.filter((_, index) => dropped.nodes.has(index))
|
|
199
|
+
.map((node) => node.id)
|
|
200
|
+
.filter((id): id is string => typeof id === "string"),
|
|
201
|
+
);
|
|
202
|
+
const next: Record<string, unknown> = { ...graph };
|
|
203
|
+
for (const section of GRAPH_SECTIONS) {
|
|
204
|
+
if (!Array.isArray(graph[section])) continue;
|
|
205
|
+
next[section] = entriesOf(graph, section).filter(
|
|
206
|
+
(entry, index) =>
|
|
207
|
+
!dropped[section].has(index) && !referencedIds(entry).some((id) => removedIds.has(id)),
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
await fs.writeFile(graphPath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
211
|
+
return notes;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Checks graphify's output instead of trusting its ignore handling, and never
|
|
216
|
+
* stops the run: secrets are taken out and everything else is kept. graph.json
|
|
217
|
+
* is pruned entry by entry; any other output file holding a secret (a report,
|
|
218
|
+
* a cache entry, the HTML view) is removed, since graphify regenerates it.
|
|
219
|
+
* Returns notes for the operator; a check that can't run becomes a note too.
|
|
220
|
+
*/
|
|
221
|
+
export async function pruneGraphifySecrets(
|
|
222
|
+
graphifyOut: string,
|
|
223
|
+
policy: SecretPolicy,
|
|
224
|
+
): Promise<string[]> {
|
|
225
|
+
if (!policy.guardrails) return [];
|
|
226
|
+
const graphPath = path.join(graphifyOut, "graph.json");
|
|
227
|
+
const notes: string[] = [];
|
|
228
|
+
try {
|
|
229
|
+
notes.push(...(await pruneGraph(graphPath, policy)));
|
|
230
|
+
} catch (error) {
|
|
231
|
+
notes.push(`graph.json could not be checked for secrets: ${(error as Error).message}`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
const entries = await fs.readdir(graphifyOut).catch((): string[] => []);
|
|
236
|
+
const others = entries
|
|
237
|
+
.filter((entry) => entry !== "graph.json")
|
|
238
|
+
.map((entry) => path.join(graphifyOut, entry));
|
|
239
|
+
const findings = await scanFiles(others);
|
|
240
|
+
const byFile = new Map<string, Set<string>>();
|
|
241
|
+
for (const finding of findings) {
|
|
242
|
+
byFile.set(finding.path, (byFile.get(finding.path) ?? new Set()).add(finding.ruleId));
|
|
243
|
+
}
|
|
244
|
+
for (const [file, rules] of byFile) {
|
|
245
|
+
// oxlint-disable-next-line no-await-in-loop -- a handful of files at most
|
|
246
|
+
await fs.rm(file, { force: true });
|
|
247
|
+
notes.push(
|
|
248
|
+
`removed ${path.relative(graphifyOut, file)}, which held a secret (${[...rules].join(", ")})`,
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
} catch (error) {
|
|
252
|
+
notes.push(`the graphify output could not be checked for secrets: ${(error as Error).message}`);
|
|
253
|
+
}
|
|
254
|
+
return notes;
|
|
255
|
+
}
|
|
@@ -12,9 +12,11 @@ import {
|
|
|
12
12
|
import { confirm } from "../../../cli/confirm";
|
|
13
13
|
import { FRAMEWORK_NAME } from "../../../framework";
|
|
14
14
|
import { getWrapperBinPath } from "../../../lib/package-paths";
|
|
15
|
+
import { resolveSecretPolicy } from "../../../runtime/policy";
|
|
15
16
|
import { isInstalledViaUvTool } from "../package-managers/uv";
|
|
16
17
|
import { stripSectionFromFile } from "../providers/agent-file-sections";
|
|
17
18
|
import { graphifySectionOptions } from "./graphify-shared";
|
|
19
|
+
import { ensureGraphifySecretExclusion } from "./graphify-secrets";
|
|
18
20
|
export { removeGraphifySection } from "./graphify-shared";
|
|
19
21
|
import {
|
|
20
22
|
mergeClaudeSettingsJsonHooks,
|
|
@@ -205,6 +207,16 @@ export function createGraphifyPlugin(deps: GraphifyPluginDeps = {}): CapabilityP
|
|
|
205
207
|
}
|
|
206
208
|
}
|
|
207
209
|
}
|
|
210
|
+
|
|
211
|
+
// Written here so it exists before any extraction; every Mate-invoked
|
|
212
|
+
// extraction rewrites it anyway and prunes whatever still gets through.
|
|
213
|
+
if (ctx.repoPath) {
|
|
214
|
+
try {
|
|
215
|
+
await ensureGraphifySecretExclusion(ctx.repoPath, resolveSecretPolicy(ctx.companionPath));
|
|
216
|
+
} catch (error) {
|
|
217
|
+
process.stderr.write(`graphify: ${(error as Error).message}\n`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
208
220
|
},
|
|
209
221
|
|
|
210
222
|
async teardown(ctx: SetupContext) {
|