@miadi/connector 0.1.8 → 0.1.10
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/README.md +12 -2
- package/dist/approve.d.ts +18 -14
- package/dist/approve.js +95 -50
- package/dist/decisions.d.ts +1 -1
- package/dist/decisions.js +42 -4
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/mission-status.d.ts +87 -0
- package/dist/mission-status.js +144 -0
- package/dist/providers.d.ts +61 -0
- package/dist/providers.js +120 -0
- package/dist/queue-state.d.ts +15 -0
- package/dist/queue-state.js +24 -0
- package/dist/records.d.ts +4 -1
- package/dist/records.js +3 -0
- package/dist/request-state.d.ts +69 -0
- package/dist/request-state.js +180 -0
- package/dist/server.d.ts +8 -0
- package/dist/server.js +16 -1
- package/dist/steer.d.ts +71 -0
- package/dist/steer.js +229 -0
- package/dist/tools.d.ts +9 -0
- package/dist/tools.js +218 -16
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/dist/server.d.ts
CHANGED
|
@@ -3,12 +3,20 @@ import type { MiadiPerson } from "./people.js";
|
|
|
3
3
|
import { Ledger } from "./store.js";
|
|
4
4
|
import type { ConnectorRecord } from "./records.js";
|
|
5
5
|
import type { ConnectorConfig } from "./config.js";
|
|
6
|
+
import type { TmuxLike } from "./steer.js";
|
|
7
|
+
import type { DiscoveryProbe } from "./providers.js";
|
|
6
8
|
export interface ConnectorDeps {
|
|
7
9
|
identify: (token: string) => Promise<MiadiPerson | null>;
|
|
8
10
|
fetchImpl?: typeof fetch;
|
|
9
11
|
/** The factory directory holding queue/ (default ~/.miadi/factory). */
|
|
10
12
|
factoryDir?: string;
|
|
11
13
|
tmuxSessions?: () => string[];
|
|
14
|
+
/** The steering surface (jgwill/Miadi#893); tests inject a fake, production uses tmux. */
|
|
15
|
+
tmux?: TmuxLike;
|
|
16
|
+
/** Provider discovery probes (jgwill/Miadi#893); tests inject fakes. */
|
|
17
|
+
providersProbe?: DiscoveryProbe;
|
|
18
|
+
/** Re-peek delay after a steer is typed (default 4000ms; tests pass 0). */
|
|
19
|
+
steerSettleMs?: number;
|
|
12
20
|
/** Person-token attempts allowed per address in 15 minutes on each sign-in form (default 30). */
|
|
13
21
|
tokenAttemptsPerWindow?: number;
|
|
14
22
|
}
|
package/dist/server.js
CHANGED
|
@@ -85,6 +85,8 @@ export function createConnectorApp(config, deps) {
|
|
|
85
85
|
fetchImpl: deps.fetchImpl,
|
|
86
86
|
factoryDir: deps.factoryDir,
|
|
87
87
|
tmuxSessions: deps.tmuxSessions,
|
|
88
|
+
tmux: deps.tmux,
|
|
89
|
+
steerSettleMs: deps.steerSettleMs,
|
|
88
90
|
};
|
|
89
91
|
registerApprovalRoutes(app, approvalDeps);
|
|
90
92
|
const decisionTokens = new DecisionTokens(config.stateDir);
|
|
@@ -94,7 +96,20 @@ export function createConnectorApp(config, deps) {
|
|
|
94
96
|
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpUrl),
|
|
95
97
|
});
|
|
96
98
|
const handle = async (req, res) => {
|
|
97
|
-
const ctx = {
|
|
99
|
+
const ctx = {
|
|
100
|
+
config,
|
|
101
|
+
miadi,
|
|
102
|
+
reviews,
|
|
103
|
+
ledger,
|
|
104
|
+
oauthState,
|
|
105
|
+
factoryDir: deps.factoryDir,
|
|
106
|
+
approval: approvalDeps,
|
|
107
|
+
decisionTokens,
|
|
108
|
+
tmux: deps.tmux,
|
|
109
|
+
tmuxSessions: deps.tmuxSessions,
|
|
110
|
+
providersProbe: deps.providersProbe,
|
|
111
|
+
steerSettleMs: deps.steerSettleMs,
|
|
112
|
+
};
|
|
98
113
|
const server = new McpServer({ name: "miadi", version: VERSION }, { instructions: INSTRUCTIONS });
|
|
99
114
|
registerTools(server, ctx);
|
|
100
115
|
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true });
|
package/dist/steer.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import type { ConnectorRecord } from "./records.js";
|
|
3
|
+
export declare const STEER_STATUSES: readonly ["prepared", "submitted", "accepted", "busy", "blocked", "wrong_pane", "stale_binding", "expired_grant", "duplicate"];
|
|
4
|
+
export type SteerStatus = (typeof STEER_STATUSES)[number];
|
|
5
|
+
/** The steering grant an approved run carries: `body.steering` on the run_request or work proposal. */
|
|
6
|
+
export interface SteeringGrant {
|
|
7
|
+
/** How many steers the approval covers. */
|
|
8
|
+
max_attempts?: number;
|
|
9
|
+
/** ISO time after which no steer runs under this grant. */
|
|
10
|
+
expires_at?: string;
|
|
11
|
+
/** The pane the seat lives in, `window.pane` (default the session's first pane). */
|
|
12
|
+
pane?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface PaneInfo {
|
|
15
|
+
/** tmux pane id, e.g. %67 — evidence only, never the address. */
|
|
16
|
+
id: string;
|
|
17
|
+
window: number;
|
|
18
|
+
pane: number;
|
|
19
|
+
command: string;
|
|
20
|
+
active: boolean;
|
|
21
|
+
}
|
|
22
|
+
/** The tmux surface the steer needs, injectable so tests never touch a live pane. */
|
|
23
|
+
export interface TmuxLike {
|
|
24
|
+
listPanes(session: string): PaneInfo[];
|
|
25
|
+
capture(target: string): string;
|
|
26
|
+
sendLiteral(target: string, text: string): void;
|
|
27
|
+
sendEnter(target: string): void;
|
|
28
|
+
}
|
|
29
|
+
/** Commands that mark a pane as an agent seat a steer may land in. */
|
|
30
|
+
export declare const AGENT_COMMANDS: string[];
|
|
31
|
+
/** Signs a seat is mid-turn; typed text would queue behind its work. */
|
|
32
|
+
export declare const BUSY_PATTERNS: RegExp[];
|
|
33
|
+
/** The production adapter. All tmux addressing is by exact session and window.pane. */
|
|
34
|
+
export declare function tmuxAdapter(exec?: typeof execFileSync): TmuxLike;
|
|
35
|
+
export declare function steeringGrantOf(run: ConnectorRecord): SteeringGrant | null;
|
|
36
|
+
export type GrantCheck = "ok" | "no_grant" | "expired_grant" | "duplicate";
|
|
37
|
+
/** Whether a grant covers one more steer. `priorAttempts` counts the steers already run under it. */
|
|
38
|
+
export declare function checkGrant(grant: SteeringGrant | null, priorAttempts: number, now?: Date): GrantCheck;
|
|
39
|
+
/** Steers already run under a grant: decided (or grant-covered) steer records that cite it. */
|
|
40
|
+
export declare function steerAttempts(records: ConnectorRecord[], grantId: string): number;
|
|
41
|
+
export declare function isBusy(capture: string, patterns?: RegExp[]): boolean;
|
|
42
|
+
export interface SteerDeps {
|
|
43
|
+
tmux: TmuxLike;
|
|
44
|
+
stateDir: string;
|
|
45
|
+
now?: () => Date;
|
|
46
|
+
settleMs?: number;
|
|
47
|
+
sleep?: (ms: number) => Promise<void>;
|
|
48
|
+
agentCommands?: string[];
|
|
49
|
+
}
|
|
50
|
+
export interface SteerResult {
|
|
51
|
+
status: SteerStatus;
|
|
52
|
+
message: string;
|
|
53
|
+
target?: string;
|
|
54
|
+
artifact?: {
|
|
55
|
+
file: string;
|
|
56
|
+
sha256: string;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** The artifact as last written, when a caller asks again with the same idempotency key. */
|
|
60
|
+
export declare function readSteerArtifact(stateDir: string, recordId: string): Record<string, unknown> | null;
|
|
61
|
+
/**
|
|
62
|
+
* Run one steer under its grant. The record is the session_steer_request; the
|
|
63
|
+
* grant record is the approved run the steer belongs to. A returned status
|
|
64
|
+
* other than `accepted`/`submitted` means nothing was typed.
|
|
65
|
+
*/
|
|
66
|
+
export declare function steerSession(deps: SteerDeps, args: {
|
|
67
|
+
record: ConnectorRecord;
|
|
68
|
+
grantRecord: ConnectorRecord;
|
|
69
|
+
priorAttempts: number;
|
|
70
|
+
authorized?: boolean;
|
|
71
|
+
}): Promise<SteerResult>;
|
package/dist/steer.js
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Approval-aware session steering (jgwill/Miadi#893, Phase 1B): one authorized
|
|
3
|
+
* follow-up to one existing live agent, never raw tmux handed to a model.
|
|
4
|
+
*
|
|
5
|
+
* The rules this file keeps:
|
|
6
|
+
* - The target is the pane the mission is bound to — the pane the grant names,
|
|
7
|
+
* else the session's first pane (lowest window, lowest pane, jgwill/Miadi#725).
|
|
8
|
+
* The active pane plays no part.
|
|
9
|
+
* - peek → scoped act → re-peek (jgwill/Miadi#345): visible pasted text is not
|
|
10
|
+
* acceptance; acceptance is the pane recaptured and the agent seen
|
|
11
|
+
* processing, with the instruction scrolled out of the input line.
|
|
12
|
+
* - Every attempt leaves a durable artifact: the instruction, its sha256, the
|
|
13
|
+
* grant it ran under, the target, the timestamps and both captures.
|
|
14
|
+
* - Pane content is evidence, never instruction: nothing an agent printed is
|
|
15
|
+
* parsed as a directive.
|
|
16
|
+
*/
|
|
17
|
+
import { createHash } from "node:crypto";
|
|
18
|
+
import { execFileSync } from "node:child_process";
|
|
19
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
export const STEER_STATUSES = [
|
|
22
|
+
"prepared",
|
|
23
|
+
"submitted",
|
|
24
|
+
"accepted",
|
|
25
|
+
"busy",
|
|
26
|
+
"blocked",
|
|
27
|
+
"wrong_pane",
|
|
28
|
+
"stale_binding",
|
|
29
|
+
"expired_grant",
|
|
30
|
+
"duplicate",
|
|
31
|
+
];
|
|
32
|
+
/** Commands that mark a pane as an agent seat a steer may land in. */
|
|
33
|
+
export const AGENT_COMMANDS = ["claude", "codex", "copilot", "pi", "gemini", "node"];
|
|
34
|
+
/** Signs a seat is mid-turn; typed text would queue behind its work. */
|
|
35
|
+
export const BUSY_PATTERNS = [/esc (to )?interrupt/i, /[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]/, /\bWorking\b/, /\bThinking\b/];
|
|
36
|
+
/** The production adapter. All tmux addressing is by exact session and window.pane. */
|
|
37
|
+
export function tmuxAdapter(exec = execFileSync) {
|
|
38
|
+
const tmux = (args) => exec("tmux", args, { encoding: "utf8", timeout: 5000, stdio: ["ignore", "pipe", "ignore"] });
|
|
39
|
+
return {
|
|
40
|
+
listPanes(session) {
|
|
41
|
+
const out = tmux(["list-panes", "-s", "-t", session, "-F", "#{pane_id}\t#{window_index}\t#{pane_index}\t#{pane_current_command}\t#{pane_active}"]);
|
|
42
|
+
return out
|
|
43
|
+
.split("\n")
|
|
44
|
+
.filter(Boolean)
|
|
45
|
+
.map((line) => {
|
|
46
|
+
const [id, w, p, command, active] = line.split("\t");
|
|
47
|
+
return { id, window: Number(w), pane: Number(p), command, active: active === "1" };
|
|
48
|
+
})
|
|
49
|
+
.sort((a, b) => a.window - b.window || a.pane - b.pane);
|
|
50
|
+
},
|
|
51
|
+
capture(target) {
|
|
52
|
+
return tmux(["capture-pane", "-p", "-t", target]);
|
|
53
|
+
},
|
|
54
|
+
// -l: the text is literal; no tmux key names in an instruction are interpreted.
|
|
55
|
+
sendLiteral(target, text) {
|
|
56
|
+
tmux(["send-keys", "-t", target, "-l", "--", text]);
|
|
57
|
+
},
|
|
58
|
+
sendEnter(target) {
|
|
59
|
+
tmux(["send-keys", "-t", target, "Enter"]);
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export function steeringGrantOf(run) {
|
|
64
|
+
const g = run.body.steering;
|
|
65
|
+
if (!g || typeof g !== "object")
|
|
66
|
+
return null;
|
|
67
|
+
const grant = g;
|
|
68
|
+
return {
|
|
69
|
+
...(typeof grant.max_attempts === "number" ? { max_attempts: grant.max_attempts } : {}),
|
|
70
|
+
...(typeof grant.expires_at === "string" ? { expires_at: grant.expires_at } : {}),
|
|
71
|
+
...(typeof grant.pane === "string" ? { pane: grant.pane } : {}),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** Whether a grant covers one more steer. `priorAttempts` counts the steers already run under it. */
|
|
75
|
+
export function checkGrant(grant, priorAttempts, now = new Date()) {
|
|
76
|
+
if (!grant)
|
|
77
|
+
return "no_grant";
|
|
78
|
+
if (grant.expires_at && !Number.isNaN(Date.parse(grant.expires_at)) && Date.parse(grant.expires_at) <= now.getTime())
|
|
79
|
+
return "expired_grant";
|
|
80
|
+
if ((grant.max_attempts ?? 0) <= priorAttempts)
|
|
81
|
+
return "duplicate";
|
|
82
|
+
return "ok";
|
|
83
|
+
}
|
|
84
|
+
/** Steers already run under a grant: decided (or grant-covered) steer records that cite it. */
|
|
85
|
+
export function steerAttempts(records, grantId) {
|
|
86
|
+
return records.filter((r) => r.kind === "session_steer_request" &&
|
|
87
|
+
r.refs.includes(grantId) &&
|
|
88
|
+
(r.approval_state === "approved" || r.approval_state === "not_required") &&
|
|
89
|
+
r.completion_receipt !== undefined).length;
|
|
90
|
+
}
|
|
91
|
+
const targetName = (session, pane) => `${session}:${pane.window}.${pane.pane}`;
|
|
92
|
+
export function isBusy(capture, patterns = BUSY_PATTERNS) {
|
|
93
|
+
const tail = capture.split("\n").slice(-25).join("\n");
|
|
94
|
+
return patterns.some((p) => p.test(tail));
|
|
95
|
+
}
|
|
96
|
+
function artifactPath(stateDir, recordId) {
|
|
97
|
+
return join(stateDir, "steers", `${recordId.replace(/[^\w-]/g, "-")}.json`);
|
|
98
|
+
}
|
|
99
|
+
function persistArtifact(stateDir, recordId, artifact) {
|
|
100
|
+
const file = artifactPath(stateDir, recordId);
|
|
101
|
+
mkdirSync(join(stateDir, "steers"), { recursive: true, mode: 0o700 });
|
|
102
|
+
const sha256 = createHash("sha256").update(JSON.stringify(artifact.instruction)).digest("hex");
|
|
103
|
+
writeFileSync(file, JSON.stringify({ ...artifact, instruction_sha256: sha256 }, null, 2) + "\n", { mode: 0o600 });
|
|
104
|
+
return { file, sha256 };
|
|
105
|
+
}
|
|
106
|
+
/** The artifact as last written, when a caller asks again with the same idempotency key. */
|
|
107
|
+
export function readSteerArtifact(stateDir, recordId) {
|
|
108
|
+
const file = artifactPath(stateDir, recordId);
|
|
109
|
+
if (!existsSync(file))
|
|
110
|
+
return null;
|
|
111
|
+
try {
|
|
112
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const CAPTURE_LINES = 40;
|
|
119
|
+
/**
|
|
120
|
+
* Run one steer under its grant. The record is the session_steer_request; the
|
|
121
|
+
* grant record is the approved run the steer belongs to. A returned status
|
|
122
|
+
* other than `accepted`/`submitted` means nothing was typed.
|
|
123
|
+
*/
|
|
124
|
+
export async function steerSession(deps, args) {
|
|
125
|
+
const now = deps.now ?? (() => new Date());
|
|
126
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
127
|
+
const agentCommands = deps.agentCommands ?? AGENT_COMMANDS;
|
|
128
|
+
const { record, grantRecord } = args;
|
|
129
|
+
const body = record.body;
|
|
130
|
+
const grant = steeringGrantOf(grantRecord);
|
|
131
|
+
const instruction = String(body.instruction ?? "");
|
|
132
|
+
const grantSession = String(grantRecord.body.session ?? "") || undefined;
|
|
133
|
+
const session = body.session ?? grantSession;
|
|
134
|
+
const finish = (status, message, extra = {}, target) => {
|
|
135
|
+
const artifact = persistArtifact(deps.stateDir, record.id, {
|
|
136
|
+
record: record.id,
|
|
137
|
+
grant: grantRecord.id,
|
|
138
|
+
instruction,
|
|
139
|
+
session,
|
|
140
|
+
status,
|
|
141
|
+
message,
|
|
142
|
+
at: now().toISOString(),
|
|
143
|
+
...extra,
|
|
144
|
+
});
|
|
145
|
+
return { status, message, ...(target ? { target } : {}), artifact };
|
|
146
|
+
};
|
|
147
|
+
if (!instruction.trim())
|
|
148
|
+
return { status: "blocked", message: "The steer carries no instruction." };
|
|
149
|
+
if (!session)
|
|
150
|
+
return finish("stale_binding", "Neither the steer nor its grant names a session; a steer never targets whichever pane is active.");
|
|
151
|
+
if (body.session && grantSession && body.session !== grantSession) {
|
|
152
|
+
return finish("stale_binding", `The steer names session "${body.session}" but its grant covers "${grantSession}".`);
|
|
153
|
+
}
|
|
154
|
+
// A person's approval of this exact steer covers one attempt even when the
|
|
155
|
+
// run's grant is exhausted; without it, the grant's own limits decide.
|
|
156
|
+
if (!args.authorized) {
|
|
157
|
+
const check = checkGrant(grant, args.priorAttempts, now());
|
|
158
|
+
if (check === "expired_grant")
|
|
159
|
+
return finish("expired_grant", `The steering grant expired at ${grant?.expires_at}.`, {}, session);
|
|
160
|
+
if (check === "duplicate") {
|
|
161
|
+
return finish("duplicate", `The grant's ${grant?.max_attempts ?? 0} steer(s) are used; a new scoped human decision is needed for more.`, {}, session);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
let panes;
|
|
165
|
+
try {
|
|
166
|
+
panes = deps.tmux.listPanes(session);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return finish("stale_binding", `No tmux session named "${session}"; the binding is stale.`, {}, session);
|
|
170
|
+
}
|
|
171
|
+
if (!panes.length)
|
|
172
|
+
return finish("stale_binding", `Session "${session}" has no panes; the binding is stale.`, {}, session);
|
|
173
|
+
const wanted = body.pane ?? grant?.pane;
|
|
174
|
+
let pane;
|
|
175
|
+
if (wanted) {
|
|
176
|
+
const [w, p] = wanted.split(".").map(Number);
|
|
177
|
+
pane = panes.find((x) => x.window === w && x.pane === p);
|
|
178
|
+
if (!pane)
|
|
179
|
+
return finish("stale_binding", `Pane ${wanted} of "${session}" is gone (stale or renumbered); nothing was typed anywhere else.`, {}, session);
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
pane = panes[0]; // listPanes is sorted: lowest window, lowest pane (jgwill/Miadi#725).
|
|
183
|
+
}
|
|
184
|
+
const target = targetName(session, pane);
|
|
185
|
+
if (!agentCommands.includes(pane.command)) {
|
|
186
|
+
return finish("wrong_pane", `Pane ${target} runs "${pane.command}", not an agent; nothing was typed.`, { pane: { ...pane } }, target);
|
|
187
|
+
}
|
|
188
|
+
let before;
|
|
189
|
+
try {
|
|
190
|
+
before = deps.tmux.capture(target);
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
return finish("blocked", `The pane could not be read (${error instanceof Error ? error.message : String(error)}); nothing was typed.`, {}, target);
|
|
194
|
+
}
|
|
195
|
+
if (isBusy(before)) {
|
|
196
|
+
return finish("busy", `The agent in ${target} is mid-turn; the steer was not typed. Ask again when it is idle.`, { peek: before.split("\n").slice(-CAPTURE_LINES) }, target);
|
|
197
|
+
}
|
|
198
|
+
deps.tmux.sendLiteral(target, instruction);
|
|
199
|
+
deps.tmux.sendEnter(target);
|
|
200
|
+
await sleep(deps.settleMs ?? 4000);
|
|
201
|
+
let after;
|
|
202
|
+
try {
|
|
203
|
+
after = deps.tmux.capture(target);
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
return finish("submitted", `Typed into ${target}, but the re-peek failed (${error instanceof Error ? error.message : String(error)}); acceptance is unconfirmed.`, { peek: before.split("\n").slice(-CAPTURE_LINES) }, target);
|
|
207
|
+
}
|
|
208
|
+
let current;
|
|
209
|
+
try {
|
|
210
|
+
current = deps.tmux.listPanes(session).find((x) => x.window === pane.window && x.pane === pane.pane);
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
current = undefined;
|
|
214
|
+
}
|
|
215
|
+
const tail = after.split("\n").slice(-CAPTURE_LINES);
|
|
216
|
+
const evidence = { peek: before.split("\n").slice(-CAPTURE_LINES), re_peek: tail };
|
|
217
|
+
if (!current || current.command !== pane.command) {
|
|
218
|
+
return finish("stale_binding", `Typed into ${target}, but the pane's command changed while sending; the seat may have exited. Acceptance is unconfirmed.`, evidence, target);
|
|
219
|
+
}
|
|
220
|
+
const firstLine = instruction.split("\n")[0].slice(0, 80);
|
|
221
|
+
const echoed = firstLine.length > 0 && after.includes(firstLine);
|
|
222
|
+
if (isBusy(after) && echoed) {
|
|
223
|
+
return finish("accepted", `The agent in ${target} is processing the steer.`, evidence, target);
|
|
224
|
+
}
|
|
225
|
+
const stillAtPrompt = tail.some((l) => firstLine.length > 0 && l.includes(firstLine)) && !isBusy(after);
|
|
226
|
+
return finish("submitted", stillAtPrompt
|
|
227
|
+
? `The instruction sits at the prompt in ${target}; it was typed but the agent has not accepted it.`
|
|
228
|
+
: `Typed into ${target}; the pane shows no processing sign, so acceptance is unconfirmed.`, evidence, target);
|
|
229
|
+
}
|
package/dist/tools.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ import type { OAuthState } from "./oauth.js";
|
|
|
7
7
|
import { type ConnectorRecord } from "./records.js";
|
|
8
8
|
import { type ApprovalDeps } from "./approve.js";
|
|
9
9
|
import { type DecisionTokens } from "./decisions.js";
|
|
10
|
+
import { type DiscoveryProbe } from "./providers.js";
|
|
11
|
+
import { type TmuxLike } from "./steer.js";
|
|
10
12
|
export interface ToolContext {
|
|
11
13
|
config: ConnectorConfig;
|
|
12
14
|
miadi: MiadiClient;
|
|
@@ -18,6 +20,13 @@ export interface ToolContext {
|
|
|
18
20
|
/** What the in-chat approval card needs to run a decided request. */
|
|
19
21
|
approval?: ApprovalDeps;
|
|
20
22
|
decisionTokens?: DecisionTokens;
|
|
23
|
+
/** The steering surface (jgwill/Miadi#893); absent, steering answers unavailable. */
|
|
24
|
+
tmux?: TmuxLike;
|
|
25
|
+
tmuxSessions?: () => string[];
|
|
26
|
+
/** Provider discovery probes; tests inject fakes (jgwill/Miadi#893). */
|
|
27
|
+
providersProbe?: DiscoveryProbe;
|
|
28
|
+
/** Re-peek delay after a steer is typed (default 4000ms; tests pass 0). */
|
|
29
|
+
steerSettleMs?: number;
|
|
21
30
|
}
|
|
22
31
|
export declare function registerTools(server: McpServer, ctx: ToolContext): void;
|
|
23
32
|
/** What get_capabilities reports for each tool. */
|