@flyingrobots/graft 0.3.5 → 0.5.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/ARCHITECTURE.md +386 -0
- package/CHANGELOG.md +69 -0
- package/CODE_OF_CONDUCT.md +65 -0
- package/README.md +153 -17
- package/bin/graft.js +4 -11
- package/docs/ADVANCED_GUIDE.md +49 -0
- package/docs/CLI.md +43 -0
- package/docs/GUIDE.md +321 -32
- package/docs/MCP.md +44 -0
- package/package.json +17 -4
- package/src/adapters/node-fs.ts +4 -0
- package/src/adapters/node-git.ts +47 -0
- package/src/adapters/node-process-runner.ts +27 -0
- package/src/cli/index-cmd.ts +86 -0
- package/src/cli/init.ts +808 -57
- package/src/cli/main.ts +437 -0
- package/src/contracts/capabilities.ts +341 -0
- package/src/contracts/causal-ontology.ts +622 -0
- package/src/contracts/causal-surface-next-action.ts +18 -0
- package/src/contracts/output-schemas.ts +1169 -0
- package/src/git/diff.ts +25 -21
- package/src/git/target-git-hook-bootstrap.ts +56 -0
- package/src/hooks/posttooluse-read.ts +21 -74
- package/src/hooks/pretooluse-read.ts +20 -56
- package/src/hooks/read-governor.ts +95 -0
- package/src/hooks/read-messages.ts +53 -0
- package/src/mcp/burden.ts +123 -0
- package/src/mcp/cache.ts +51 -0
- package/src/mcp/cached-file.ts +10 -8
- package/src/mcp/context.ts +67 -2
- package/src/mcp/daemon-control-plane.ts +554 -0
- package/src/mcp/daemon-job-scheduler.ts +279 -0
- package/src/mcp/daemon-repos.ts +216 -0
- package/src/mcp/daemon-server.ts +396 -0
- package/src/mcp/daemon-worker-pool.ts +310 -0
- package/src/mcp/daemon-worker-process.ts +52 -0
- package/src/mcp/metrics.ts +108 -1
- package/src/mcp/monitor-tick-job.ts +99 -0
- package/src/mcp/persisted-local-history.ts +1246 -0
- package/src/mcp/persistent-monitor-runtime.ts +549 -0
- package/src/mcp/policy.ts +84 -0
- package/src/mcp/receipt.ts +82 -12
- package/src/mcp/repo-concurrency.ts +318 -0
- package/src/mcp/repo-state.ts +777 -0
- package/src/mcp/repo-tool-job.ts +302 -0
- package/src/mcp/run-capture-config.ts +33 -0
- package/src/mcp/runtime-causal-context.ts +72 -0
- package/src/mcp/runtime-observability.ts +219 -0
- package/src/mcp/runtime-staged-target.ts +161 -0
- package/src/mcp/runtime-workspace-overlay.ts +255 -0
- package/src/mcp/semantic-transition-guidance.ts +60 -0
- package/src/mcp/semantic-transition-summary.ts +130 -0
- package/src/mcp/server.ts +704 -45
- package/src/mcp/stdio-server.ts +12 -0
- package/src/mcp/stdio.ts +2 -5
- package/src/mcp/tools/activity-view.ts +325 -0
- package/src/mcp/tools/causal-attach.ts +67 -0
- package/src/mcp/tools/causal-status.ts +58 -0
- package/src/mcp/tools/changed-since.ts +13 -11
- package/src/mcp/tools/code-find.ts +164 -0
- package/src/mcp/tools/code-refs.ts +466 -0
- package/src/mcp/tools/code-show.ts +252 -0
- package/src/mcp/tools/daemon-monitors.ts +14 -0
- package/src/mcp/tools/daemon-repos.ts +22 -0
- package/src/mcp/tools/daemon-sessions.ts +14 -0
- package/src/mcp/tools/daemon-status.ts +12 -0
- package/src/mcp/tools/doctor.ts +45 -2
- package/src/mcp/tools/explain.ts +4 -0
- package/src/mcp/tools/file-outline.ts +7 -3
- package/src/mcp/tools/git-files.ts +73 -0
- package/src/mcp/tools/graft-diff.ts +12 -4
- package/src/mcp/tools/map.ts +136 -0
- package/src/mcp/tools/monitor-pause.ts +18 -0
- package/src/mcp/tools/monitor-resume.ts +18 -0
- package/src/mcp/tools/monitor-start.ts +20 -0
- package/src/mcp/tools/monitor-stop.ts +18 -0
- package/src/mcp/tools/precision-match.ts +51 -0
- package/src/mcp/tools/precision-query.ts +127 -0
- package/src/mcp/tools/precision.ts +312 -0
- package/src/mcp/tools/run-capture.ts +126 -44
- package/src/mcp/tools/safe-read.ts +14 -12
- package/src/mcp/tools/since.ts +49 -0
- package/src/mcp/tools/state.ts +11 -3
- package/src/mcp/tools/stats.ts +5 -1
- package/src/mcp/tools/workspace-authorizations.ts +14 -0
- package/src/mcp/tools/workspace-authorize.ts +20 -0
- package/src/mcp/tools/workspace-bind.ts +25 -0
- package/src/mcp/tools/workspace-rebind.ts +25 -0
- package/src/mcp/tools/workspace-revoke.ts +18 -0
- package/src/mcp/tools/workspace-status.ts +12 -0
- package/src/mcp/warp-pool.ts +36 -0
- package/src/mcp/workspace-router.ts +984 -0
- package/src/operations/file-outline.ts +12 -2
- package/src/operations/graft-diff.ts +56 -10
- package/src/operations/safe-read.ts +27 -4
- package/src/operations/state.ts +6 -9
- package/src/parser/lang.ts +19 -3
- package/src/parser/outline.ts +191 -2
- package/src/parser/types.ts +9 -1
- package/src/policy/types.ts +4 -3
- package/src/ports/filesystem.ts +1 -0
- package/src/ports/git.ts +16 -0
- package/src/ports/process-runner.ts +22 -0
- package/src/release/security-gate.ts +102 -0
- package/src/session/tracker.ts +31 -0
- package/src/version.ts +3 -0
- package/src/warp/indexer.ts +513 -0
- package/src/warp/observers.ts +105 -0
- package/src/warp/open.ts +31 -0
- package/src/warp/plumbing.d.ts +15 -0
- package/src/warp/writer-id.ts +30 -0
|
@@ -1,66 +1,148 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
-
import { execFileSync } from "node:child_process";
|
|
3
2
|
import { z } from "zod";
|
|
4
3
|
import type { ToolDefinition, ToolContext, ToolHandler } from "../context.js";
|
|
5
4
|
|
|
5
|
+
const RUN_CAPTURE_POLICY_BOUNDARY = {
|
|
6
|
+
kind: "shell_escape_hatch",
|
|
7
|
+
boundedReadContract: false,
|
|
8
|
+
policyEnforced: false,
|
|
9
|
+
} as const;
|
|
10
|
+
|
|
11
|
+
const PRIVATE_KEY_BLOCK_RE = /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g;
|
|
12
|
+
const SECRET_ASSIGNMENT_RE = /(\b(?:api[_-]?key|access[_-]?token|auth(?:orization)?|bearer|token|secret|password|passwd|private[_-]?key|client[_-]?secret|session[_-]?key)\b\s*[:=]\s*)([^\r\n]+)/gi;
|
|
13
|
+
const BEARER_TOKEN_RE = /(\bbearer\s+)([A-Za-z0-9._~+/=-]{8,})/gi;
|
|
14
|
+
|
|
15
|
+
function tailOutput(output: string, tail: number): {
|
|
16
|
+
readonly tailed: string;
|
|
17
|
+
readonly totalLines: number;
|
|
18
|
+
} {
|
|
19
|
+
const lines = output.split("\n");
|
|
20
|
+
return {
|
|
21
|
+
tailed: lines.slice(-tail).join("\n"),
|
|
22
|
+
totalLines: lines.length,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function renderCaptureError(result: {
|
|
27
|
+
readonly status: number | null;
|
|
28
|
+
readonly stderr: string;
|
|
29
|
+
readonly error?: Error;
|
|
30
|
+
}): string {
|
|
31
|
+
if (result.error !== undefined) {
|
|
32
|
+
return result.error.message;
|
|
33
|
+
}
|
|
34
|
+
const stderr = result.stderr.trim();
|
|
35
|
+
if (stderr.length > 0) {
|
|
36
|
+
return stderr;
|
|
37
|
+
}
|
|
38
|
+
return `Command exited with status ${String(result.status)}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function redactForLog(output: string): {
|
|
42
|
+
readonly value: string;
|
|
43
|
+
readonly redactions: number;
|
|
44
|
+
} {
|
|
45
|
+
let redactions = 0;
|
|
46
|
+
let value = output.replace(PRIVATE_KEY_BLOCK_RE, () => {
|
|
47
|
+
redactions++;
|
|
48
|
+
return "[REDACTED PRIVATE KEY BLOCK]";
|
|
49
|
+
});
|
|
50
|
+
value = value.replace(SECRET_ASSIGNMENT_RE, (_match, prefix: string) => {
|
|
51
|
+
redactions++;
|
|
52
|
+
return `${prefix}[REDACTED]`;
|
|
53
|
+
});
|
|
54
|
+
value = value.replace(BEARER_TOKEN_RE, (_match, prefix: string) => {
|
|
55
|
+
redactions++;
|
|
56
|
+
return `${prefix}[REDACTED]`;
|
|
57
|
+
});
|
|
58
|
+
return { value, redactions };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function persistCaptureLog(ctx: ToolContext, output: string): Promise<{
|
|
62
|
+
readonly logPath: string | null;
|
|
63
|
+
readonly logRedactions: number;
|
|
64
|
+
}> {
|
|
65
|
+
if (!ctx.runCapture.persistLogs) {
|
|
66
|
+
return { logPath: null, logRedactions: 0 };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const logPath = path.join(ctx.graftDir, "logs", "capture.log");
|
|
70
|
+
const persisted = ctx.runCapture.redactLogs ? redactForLog(output) : { value: output, redactions: 0 };
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
await ctx.fs.mkdir(path.dirname(logPath), { recursive: true });
|
|
74
|
+
await ctx.fs.writeFile(logPath, persisted.value, "utf-8");
|
|
75
|
+
return {
|
|
76
|
+
logPath,
|
|
77
|
+
logRedactions: persisted.redactions,
|
|
78
|
+
};
|
|
79
|
+
} catch {
|
|
80
|
+
return { logPath: null, logRedactions: 0 };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
6
84
|
export const runCaptureTool: ToolDefinition = {
|
|
7
85
|
name: "run_capture",
|
|
8
86
|
description:
|
|
9
87
|
"Execute a shell command and return the last N lines of output " +
|
|
10
|
-
"(default 60).
|
|
11
|
-
"
|
|
88
|
+
"(default 60). Responses include an explicit policy boundary. " +
|
|
89
|
+
"Persisted logs can be disabled and obvious secrets are redacted " +
|
|
90
|
+
"before writing to .graft/logs/capture.log.",
|
|
12
91
|
schema: { command: z.string(), tail: z.number().optional() },
|
|
13
92
|
createHandler(ctx: ToolContext): ToolHandler {
|
|
14
93
|
return async (args) => {
|
|
15
94
|
const command = args["command"] as string;
|
|
16
95
|
const tail = Math.max(1, Math.floor((args["tail"] as number | undefined) ?? 60));
|
|
17
|
-
|
|
18
|
-
// and synchronous execution simplifies stdout/stderr capture with timeout.
|
|
19
|
-
let output: string;
|
|
20
|
-
try {
|
|
21
|
-
output = execFileSync("sh", ["-c", command], {
|
|
22
|
-
cwd: ctx.projectRoot,
|
|
23
|
-
encoding: "utf-8",
|
|
24
|
-
timeout: 30000,
|
|
25
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
26
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
27
|
-
});
|
|
28
|
-
} catch (err: unknown) {
|
|
29
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
30
|
-
const stdout = (err as { stdout?: string }).stdout ?? "";
|
|
31
|
-
const stderr = (err as { stderr?: string }).stderr ?? "";
|
|
32
|
-
// Return whatever stdout was captured before failure
|
|
33
|
-
const tailed = typeof stdout === "string"
|
|
34
|
-
? stdout.split("\n").slice(-tail).join("\n")
|
|
35
|
-
: "";
|
|
36
|
-
const totalLines = typeof stdout === "string" ? stdout.split("\n").length : 0;
|
|
96
|
+
if (!ctx.runCapture.enabled) {
|
|
37
97
|
return ctx.respond("run_capture", {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
98
|
+
output: "",
|
|
99
|
+
totalLines: 0,
|
|
100
|
+
tailedLines: 0,
|
|
101
|
+
logPath: null,
|
|
102
|
+
logRedactions: 0,
|
|
103
|
+
logPersistenceEnabled: false,
|
|
104
|
+
truncated: false,
|
|
105
|
+
disabled: true,
|
|
106
|
+
error: "run_capture is disabled by configuration",
|
|
107
|
+
policyBoundary: RUN_CAPTURE_POLICY_BOUNDARY,
|
|
44
108
|
});
|
|
45
109
|
}
|
|
110
|
+
const result = ctx.process.run({
|
|
111
|
+
command: "sh",
|
|
112
|
+
args: ["-c", command],
|
|
113
|
+
cwd: ctx.projectRoot,
|
|
114
|
+
timeoutMs: 30000,
|
|
115
|
+
maxBufferBytes: 10 * 1024 * 1024,
|
|
116
|
+
});
|
|
46
117
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
118
|
+
if (result.error !== undefined || result.status !== 0) {
|
|
119
|
+
const tailedOutput = tailOutput(result.stdout, tail);
|
|
120
|
+
return ctx.respond("run_capture", {
|
|
121
|
+
error: renderCaptureError(result),
|
|
122
|
+
output: tailedOutput.tailed,
|
|
123
|
+
totalLines: tailedOutput.totalLines,
|
|
124
|
+
tailedLines: Math.min(tail, tailedOutput.totalLines),
|
|
125
|
+
logPath: null,
|
|
126
|
+
logRedactions: 0,
|
|
127
|
+
logPersistenceEnabled: ctx.runCapture.persistLogs,
|
|
128
|
+
truncated: tailedOutput.totalLines > tail,
|
|
129
|
+
stderr: result.stderr.slice(0, 2000),
|
|
130
|
+
policyBoundary: RUN_CAPTURE_POLICY_BOUNDARY,
|
|
131
|
+
});
|
|
57
132
|
}
|
|
133
|
+
|
|
134
|
+
const output = result.stdout;
|
|
135
|
+
const tailedOutput = tailOutput(output, tail);
|
|
136
|
+
const persisted = await persistCaptureLog(ctx, output);
|
|
58
137
|
return ctx.respond("run_capture", {
|
|
59
|
-
output: tailed,
|
|
60
|
-
totalLines:
|
|
61
|
-
tailedLines: Math.min(tail,
|
|
62
|
-
logPath:
|
|
63
|
-
|
|
138
|
+
output: tailedOutput.tailed,
|
|
139
|
+
totalLines: tailedOutput.totalLines,
|
|
140
|
+
tailedLines: Math.min(tail, tailedOutput.totalLines),
|
|
141
|
+
logPath: persisted.logPath,
|
|
142
|
+
logRedactions: persisted.logRedactions,
|
|
143
|
+
logPersistenceEnabled: ctx.runCapture.persistLogs,
|
|
144
|
+
truncated: tailedOutput.totalLines > tail,
|
|
145
|
+
policyBoundary: RUN_CAPTURE_POLICY_BOUNDARY,
|
|
64
146
|
});
|
|
65
147
|
};
|
|
66
148
|
},
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { safeRead } from "../../operations/safe-read.js";
|
|
3
3
|
import type { SafeReadResult } from "../../operations/safe-read.js";
|
|
4
|
-
import { evaluatePolicy } from "../../policy/evaluate.js";
|
|
5
4
|
import { RefusedResult } from "../../policy/types.js";
|
|
6
5
|
import { diffOutlines } from "../../parser/diff.js";
|
|
7
6
|
import { CachedFile } from "../cached-file.js";
|
|
8
7
|
import type { Metrics } from "../metrics.js";
|
|
8
|
+
import { evaluateMcpPolicy, toPolicyPath } from "../policy.js";
|
|
9
9
|
import type { ToolDefinition, ToolContext, ToolHandler } from "../context.js";
|
|
10
10
|
|
|
11
11
|
const PROJECTION_METRICS: Readonly<Record<string, ((m: Metrics) => void) | undefined>> = {
|
|
@@ -32,21 +32,18 @@ export const safeReadTool: ToolDefinition = {
|
|
|
32
32
|
// eliminating TOCTOU races where the file changes between reads.
|
|
33
33
|
let cf: CachedFile | null = null;
|
|
34
34
|
try {
|
|
35
|
-
const rawContent = ctx.fs.
|
|
35
|
+
const rawContent = await ctx.fs.readFile(filePath, "utf-8");
|
|
36
36
|
cf = new CachedFile(filePath, rawContent);
|
|
37
37
|
} catch {
|
|
38
38
|
// File doesn't exist or can't be read — proceed to safeRead for error handling
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
// Check cache if we could read the file
|
|
42
|
-
if (cf
|
|
42
|
+
if (cf?.supportsOutline === true) {
|
|
43
43
|
const cacheResult = ctx.cache.check(filePath, cf.rawContent);
|
|
44
44
|
if (cacheResult.hit) {
|
|
45
45
|
// Defense: re-check policy before returning cached data.
|
|
46
|
-
const policy =
|
|
47
|
-
{ path: filePath, lines: cf.actual.lines, bytes: cf.actual.bytes },
|
|
48
|
-
{ sessionDepth: ctx.session.getSessionDepth(), budgetRemaining: ctx.session.getBudget()?.remaining },
|
|
49
|
-
);
|
|
46
|
+
const policy = evaluateMcpPolicy(ctx, filePath, cf.actual);
|
|
50
47
|
if (policy instanceof RefusedResult) {
|
|
51
48
|
ctx.metrics.recordRefusal();
|
|
52
49
|
return ctx.respond("safe_read", {
|
|
@@ -77,10 +74,7 @@ export const safeReadTool: ToolDefinition = {
|
|
|
77
74
|
// File changed since last observation — compute structural diff
|
|
78
75
|
if (cacheResult.stale !== null) {
|
|
79
76
|
// Defense: re-check policy before returning structural data.
|
|
80
|
-
const policy =
|
|
81
|
-
{ path: filePath, lines: cf.actual.lines, bytes: cf.actual.bytes },
|
|
82
|
-
{ sessionDepth: ctx.session.getSessionDepth(), budgetRemaining: ctx.session.getBudget()?.remaining },
|
|
83
|
-
);
|
|
77
|
+
const policy = evaluateMcpPolicy(ctx, filePath, cf.actual);
|
|
84
78
|
if (policy instanceof RefusedResult) {
|
|
85
79
|
ctx.metrics.recordRefusal();
|
|
86
80
|
return ctx.respond("safe_read", {
|
|
@@ -117,6 +111,8 @@ export const safeReadTool: ToolDefinition = {
|
|
|
117
111
|
codec: ctx.codec,
|
|
118
112
|
content: cf?.rawContent,
|
|
119
113
|
intent: args["intent"] as string | undefined,
|
|
114
|
+
policyPath: toPolicyPath(ctx.projectRoot, filePath),
|
|
115
|
+
graftignorePatterns: [...ctx.graftignorePatterns],
|
|
120
116
|
sessionDepth: ctx.session.getSessionDepth(),
|
|
121
117
|
budgetRemaining: ctx.session.getBudget()?.remaining,
|
|
122
118
|
});
|
|
@@ -125,7 +121,13 @@ export const safeReadTool: ToolDefinition = {
|
|
|
125
121
|
|
|
126
122
|
// Record observation for cacheable projections — uses CachedFile
|
|
127
123
|
// outline (no re-read) to eliminate the snapshot race.
|
|
128
|
-
if (
|
|
124
|
+
if (
|
|
125
|
+
cf !== null &&
|
|
126
|
+
cf.supportsOutline &&
|
|
127
|
+
result.actual !== undefined &&
|
|
128
|
+
CACHEABLE_PROJECTIONS.has(result.projection) &&
|
|
129
|
+
result.reason !== "UNSUPPORTED_LANGUAGE"
|
|
130
|
+
) {
|
|
129
131
|
ctx.cache.record(filePath, cf.hash, cf.outline, cf.jumpTable, result.actual);
|
|
130
132
|
}
|
|
131
133
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { graftDiff } from "../../operations/graft-diff.js";
|
|
3
|
+
import type { ToolDefinition, ToolContext, ToolHandler } from "../context.js";
|
|
4
|
+
import { evaluateMcpRefusal } from "../policy.js";
|
|
5
|
+
|
|
6
|
+
export const sinceTool: ToolDefinition = {
|
|
7
|
+
name: "graft_since",
|
|
8
|
+
description:
|
|
9
|
+
"Structural changes since a git ref. Shows symbols added, removed, " +
|
|
10
|
+
"and changed per file — not line hunks. Includes per-file summary " +
|
|
11
|
+
"lines for quick triage. Defaults to HEAD as the comparison target.",
|
|
12
|
+
schema: {
|
|
13
|
+
base: z.string(),
|
|
14
|
+
head: z.string().optional(),
|
|
15
|
+
},
|
|
16
|
+
createHandler(ctx: ToolContext): ToolHandler {
|
|
17
|
+
return async (args) => {
|
|
18
|
+
const base = args["base"] as string;
|
|
19
|
+
const head = (args["head"] as string | undefined) ?? "HEAD";
|
|
20
|
+
|
|
21
|
+
const result = await graftDiff({
|
|
22
|
+
cwd: ctx.projectRoot,
|
|
23
|
+
fs: ctx.fs,
|
|
24
|
+
git: ctx.git,
|
|
25
|
+
resolveWorkingTreePath: (filePath) => ctx.resolvePath(filePath),
|
|
26
|
+
base,
|
|
27
|
+
head,
|
|
28
|
+
refusalCheck: (filePath, actual) => evaluateMcpRefusal(ctx, filePath, actual),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Aggregate symbol-level changes across all files
|
|
32
|
+
let totalAdded = 0;
|
|
33
|
+
let totalRemoved = 0;
|
|
34
|
+
let totalChanged = 0;
|
|
35
|
+
|
|
36
|
+
for (const file of result.files) {
|
|
37
|
+
totalAdded += file.diff.added.length;
|
|
38
|
+
totalRemoved += file.diff.removed.length;
|
|
39
|
+
totalChanged += file.diff.changed.length;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return ctx.respond("graft_since", {
|
|
43
|
+
...result,
|
|
44
|
+
summary: `+${String(totalAdded)} added, -${String(totalRemoved)} removed, ~${String(totalChanged)} changed across ${String(result.files.length)} files`,
|
|
45
|
+
layer: "ref_view",
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
};
|
package/src/mcp/tools/state.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
1
2
|
import { z } from "zod";
|
|
2
|
-
import { stateSave, stateLoad } from "../../operations/state.js";
|
|
3
|
+
import { STATE_FILENAME, stateSave, stateLoad } from "../../operations/state.js";
|
|
3
4
|
import type { ToolDefinition, ToolContext, ToolHandler } from "../context.js";
|
|
4
5
|
|
|
5
6
|
export const stateSaveTool: ToolDefinition = {
|
|
@@ -10,7 +11,11 @@ export const stateSaveTool: ToolDefinition = {
|
|
|
10
11
|
schema: { content: z.string() },
|
|
11
12
|
createHandler(ctx: ToolContext): ToolHandler {
|
|
12
13
|
return async (args) => {
|
|
13
|
-
const result = await stateSave(args["content"] as string, {
|
|
14
|
+
const result = await stateSave(args["content"] as string, {
|
|
15
|
+
stateDir: ctx.graftDir,
|
|
16
|
+
statePath: path.join(ctx.graftDir, STATE_FILENAME),
|
|
17
|
+
fs: ctx.fs,
|
|
18
|
+
});
|
|
14
19
|
return ctx.respond("state_save", result as Record<string, unknown>);
|
|
15
20
|
};
|
|
16
21
|
},
|
|
@@ -23,7 +28,10 @@ export const stateLoadTool: ToolDefinition = {
|
|
|
23
28
|
"been saved.",
|
|
24
29
|
createHandler(ctx: ToolContext): ToolHandler {
|
|
25
30
|
return async () => {
|
|
26
|
-
const result = await stateLoad({
|
|
31
|
+
const result = await stateLoad({
|
|
32
|
+
statePath: path.join(ctx.graftDir, STATE_FILENAME),
|
|
33
|
+
fs: ctx.fs,
|
|
34
|
+
});
|
|
27
35
|
return ctx.respond("state_load", result as Record<string, unknown>);
|
|
28
36
|
};
|
|
29
37
|
},
|
package/src/mcp/tools/stats.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { totalNonReadBytesReturned } from "../burden.js";
|
|
1
2
|
import type { ToolDefinition, ToolContext, ToolHandler } from "../context.js";
|
|
2
3
|
|
|
3
4
|
export const statsTool: ToolDefinition = {
|
|
4
5
|
name: "stats",
|
|
5
6
|
description:
|
|
6
7
|
"Decision metrics for the current session. Total reads, outlines, " +
|
|
7
|
-
"refusals, cache hits, and
|
|
8
|
+
"refusals, cache hits, bytes avoided, and burden by tool kind.",
|
|
8
9
|
createHandler(ctx: ToolContext): ToolHandler {
|
|
9
10
|
return () => {
|
|
10
11
|
const snap = ctx.metrics.snapshot();
|
|
@@ -13,7 +14,10 @@ export const statsTool: ToolDefinition = {
|
|
|
13
14
|
totalOutlines: snap.outlines,
|
|
14
15
|
totalRefusals: snap.refusals,
|
|
15
16
|
totalCacheHits: snap.cacheHits,
|
|
17
|
+
totalBytesReturned: snap.bytesReturned,
|
|
16
18
|
totalBytesAvoidedByCache: snap.bytesAvoided,
|
|
19
|
+
totalNonReadBytesReturned: totalNonReadBytesReturned(snap.burdenByKind),
|
|
20
|
+
burdenByKind: snap.burdenByKind,
|
|
17
21
|
});
|
|
18
22
|
};
|
|
19
23
|
},
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ToolDefinition, ToolContext, ToolHandler } from "../context.js";
|
|
2
|
+
|
|
3
|
+
export const workspaceAuthorizationsTool: ToolDefinition = {
|
|
4
|
+
name: "workspace_authorizations",
|
|
5
|
+
description:
|
|
6
|
+
"List daemon-authorized workspaces, their capability posture, and active bound-session counts.",
|
|
7
|
+
createHandler(ctx: ToolContext): ToolHandler {
|
|
8
|
+
return async () => {
|
|
9
|
+
return ctx.respond("workspace_authorizations", {
|
|
10
|
+
workspaces: await ctx.listWorkspaceAuthorizations(),
|
|
11
|
+
});
|
|
12
|
+
};
|
|
13
|
+
},
|
|
14
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { ToolDefinition, ToolContext, ToolHandler } from "../context.js";
|
|
3
|
+
|
|
4
|
+
export const workspaceAuthorizeTool: ToolDefinition = {
|
|
5
|
+
name: "workspace_authorize",
|
|
6
|
+
description:
|
|
7
|
+
"Authorize a workspace for daemon binding and optionally change its daemon capability posture.",
|
|
8
|
+
schema: {
|
|
9
|
+
cwd: z.string(),
|
|
10
|
+
runCapture: z.boolean().optional(),
|
|
11
|
+
},
|
|
12
|
+
createHandler(ctx: ToolContext): ToolHandler {
|
|
13
|
+
return async (args) => {
|
|
14
|
+
return ctx.respond("workspace_authorize", { ...await ctx.authorizeWorkspace({
|
|
15
|
+
cwd: args["cwd"] as string,
|
|
16
|
+
runCapture: args["runCapture"] as boolean | undefined,
|
|
17
|
+
}) });
|
|
18
|
+
};
|
|
19
|
+
},
|
|
20
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { ToolDefinition, ToolContext, ToolHandler } from "../context.js";
|
|
3
|
+
|
|
4
|
+
export const workspaceBindTool: ToolDefinition = {
|
|
5
|
+
name: "workspace_bind",
|
|
6
|
+
description:
|
|
7
|
+
"Bind the current daemon session to a workspace by resolving repo and worktree identity server-side.",
|
|
8
|
+
schema: {
|
|
9
|
+
cwd: z.string(),
|
|
10
|
+
worktreeRoot: z.string().optional(),
|
|
11
|
+
gitCommonDir: z.string().optional(),
|
|
12
|
+
repoId: z.string().optional(),
|
|
13
|
+
},
|
|
14
|
+
createHandler(ctx: ToolContext): ToolHandler {
|
|
15
|
+
return async (args) => {
|
|
16
|
+
const result = await ctx.bindWorkspace({
|
|
17
|
+
cwd: args["cwd"] as string,
|
|
18
|
+
worktreeRoot: args["worktreeRoot"] as string | undefined,
|
|
19
|
+
gitCommonDir: args["gitCommonDir"] as string | undefined,
|
|
20
|
+
repoId: args["repoId"] as string | undefined,
|
|
21
|
+
}, "workspace_bind");
|
|
22
|
+
return ctx.respond("workspace_bind", { ...result });
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { ToolDefinition, ToolContext, ToolHandler } from "../context.js";
|
|
3
|
+
|
|
4
|
+
export const workspaceRebindTool: ToolDefinition = {
|
|
5
|
+
name: "workspace_rebind",
|
|
6
|
+
description:
|
|
7
|
+
"Rebind the current daemon session to a different workspace and start a fresh session-local slice.",
|
|
8
|
+
schema: {
|
|
9
|
+
cwd: z.string(),
|
|
10
|
+
worktreeRoot: z.string().optional(),
|
|
11
|
+
gitCommonDir: z.string().optional(),
|
|
12
|
+
repoId: z.string().optional(),
|
|
13
|
+
},
|
|
14
|
+
createHandler(ctx: ToolContext): ToolHandler {
|
|
15
|
+
return async (args) => {
|
|
16
|
+
const result = await ctx.rebindWorkspace({
|
|
17
|
+
cwd: args["cwd"] as string,
|
|
18
|
+
worktreeRoot: args["worktreeRoot"] as string | undefined,
|
|
19
|
+
gitCommonDir: args["gitCommonDir"] as string | undefined,
|
|
20
|
+
repoId: args["repoId"] as string | undefined,
|
|
21
|
+
}, "workspace_rebind");
|
|
22
|
+
return ctx.respond("workspace_rebind", { ...result });
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { ToolDefinition, ToolContext, ToolHandler } from "../context.js";
|
|
3
|
+
|
|
4
|
+
export const workspaceRevokeTool: ToolDefinition = {
|
|
5
|
+
name: "workspace_revoke",
|
|
6
|
+
description:
|
|
7
|
+
"Revoke daemon authorization for a workspace while leaving any already-open sessions visible to the control plane.",
|
|
8
|
+
schema: {
|
|
9
|
+
cwd: z.string(),
|
|
10
|
+
},
|
|
11
|
+
createHandler(ctx: ToolContext): ToolHandler {
|
|
12
|
+
return async (args) => {
|
|
13
|
+
return ctx.respond("workspace_revoke", { ...await ctx.revokeWorkspace({
|
|
14
|
+
cwd: args["cwd"] as string,
|
|
15
|
+
}) });
|
|
16
|
+
};
|
|
17
|
+
},
|
|
18
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ToolDefinition, ToolContext, ToolHandler } from "../context.js";
|
|
2
|
+
|
|
3
|
+
export const workspaceStatusTool: ToolDefinition = {
|
|
4
|
+
name: "workspace_status",
|
|
5
|
+
description:
|
|
6
|
+
"Return the current daemon workspace binding state and resolved capability profile.",
|
|
7
|
+
createHandler(ctx: ToolContext): ToolHandler {
|
|
8
|
+
return () => {
|
|
9
|
+
return ctx.respond("workspace_status", { ...ctx.getWorkspaceStatus() });
|
|
10
|
+
};
|
|
11
|
+
},
|
|
12
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type WarpApp from "@git-stunts/git-warp";
|
|
2
|
+
import { DEFAULT_WARP_WRITER_ID } from "../warp/writer-id.js";
|
|
3
|
+
|
|
4
|
+
export interface WarpPool {
|
|
5
|
+
getOrOpen(repoId: string, worktreeRoot: string, writerId?: string): Promise<WarpApp>;
|
|
6
|
+
size(): number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class InMemoryWarpPool implements WarpPool {
|
|
10
|
+
private readonly opened = new Map<string, Map<string, Promise<WarpApp>>>();
|
|
11
|
+
|
|
12
|
+
constructor(private readonly openWarp: (worktreeRoot: string, writerId: string) => Promise<WarpApp>) {}
|
|
13
|
+
|
|
14
|
+
getOrOpen(repoId: string, worktreeRoot: string, writerId: string = DEFAULT_WARP_WRITER_ID): Promise<WarpApp> {
|
|
15
|
+
const repoHandles = this.opened.get(repoId);
|
|
16
|
+
const cached = repoHandles?.get(writerId);
|
|
17
|
+
if (cached !== undefined) return cached;
|
|
18
|
+
|
|
19
|
+
const nextRepoHandles = repoHandles ?? new Map<string, Promise<WarpApp>>();
|
|
20
|
+
const opened = this.openWarp(worktreeRoot, writerId).catch((error: unknown) => {
|
|
21
|
+
const current = this.opened.get(repoId);
|
|
22
|
+
current?.delete(writerId);
|
|
23
|
+
if (current?.size === 0) {
|
|
24
|
+
this.opened.delete(repoId);
|
|
25
|
+
}
|
|
26
|
+
throw error;
|
|
27
|
+
});
|
|
28
|
+
nextRepoHandles.set(writerId, opened);
|
|
29
|
+
this.opened.set(repoId, nextRepoHandles);
|
|
30
|
+
return opened;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
size(): number {
|
|
34
|
+
return this.opened.size;
|
|
35
|
+
}
|
|
36
|
+
}
|