@dalmasonto/taskflow-mcp 1.0.33 → 2.0.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.
Files changed (99) hide show
  1. package/README.md +138 -193
  2. package/dist/attachment-download.d.ts +74 -0
  3. package/dist/attachment-download.js +193 -0
  4. package/dist/attachment-download.js.map +1 -0
  5. package/dist/attachments.d.ts +23 -0
  6. package/dist/attachments.js +66 -0
  7. package/dist/attachments.js.map +1 -0
  8. package/dist/client.d.ts +206 -0
  9. package/dist/client.js +279 -0
  10. package/dist/client.js.map +1 -0
  11. package/dist/config.d.ts +137 -18
  12. package/dist/config.js +187 -106
  13. package/dist/config.js.map +1 -0
  14. package/dist/connect.d.ts +89 -0
  15. package/dist/connect.js +269 -0
  16. package/dist/connect.js.map +1 -0
  17. package/dist/doctor.d.ts +24 -0
  18. package/dist/doctor.js +120 -0
  19. package/dist/doctor.js.map +1 -0
  20. package/dist/events.d.ts +186 -0
  21. package/dist/events.js +407 -0
  22. package/dist/events.js.map +1 -0
  23. package/dist/index.d.ts +7 -1
  24. package/dist/index.js +124 -184
  25. package/dist/index.js.map +1 -0
  26. package/dist/instructions.d.ts +12 -0
  27. package/dist/instructions.js +114 -0
  28. package/dist/instructions.js.map +1 -0
  29. package/dist/mint.d.ts +62 -0
  30. package/dist/mint.js +135 -0
  31. package/dist/mint.js.map +1 -0
  32. package/dist/mirror.d.ts +68 -0
  33. package/dist/mirror.js +103 -0
  34. package/dist/mirror.js.map +1 -0
  35. package/dist/pane-queue.d.ts +29 -0
  36. package/dist/pane-queue.js +35 -0
  37. package/dist/pane-queue.js.map +1 -0
  38. package/dist/prompts.d.ts +79 -0
  39. package/dist/prompts.js +211 -0
  40. package/dist/prompts.js.map +1 -0
  41. package/dist/resolve.d.ts +72 -0
  42. package/dist/resolve.js +89 -0
  43. package/dist/resolve.js.map +1 -0
  44. package/dist/runtime.d.ts +54 -0
  45. package/dist/runtime.js +321 -0
  46. package/dist/runtime.js.map +1 -0
  47. package/dist/server.d.ts +56 -0
  48. package/dist/server.js +793 -0
  49. package/dist/server.js.map +1 -0
  50. package/dist/session-identifier.d.ts +48 -0
  51. package/dist/session-identifier.js +44 -0
  52. package/dist/session-identifier.js.map +1 -0
  53. package/dist/sessions-store.d.ts +38 -0
  54. package/dist/sessions-store.js +88 -0
  55. package/dist/sessions-store.js.map +1 -0
  56. package/dist/tmux.d.ts +200 -0
  57. package/dist/tmux.js +580 -0
  58. package/dist/tmux.js.map +1 -0
  59. package/hooks/metadata.mjs +99 -0
  60. package/hooks/permission-prompt.mjs +100 -0
  61. package/hooks/taskflow-hook.mjs +499 -0
  62. package/hooks/tool-logging.mjs +63 -0
  63. package/package.json +38 -29
  64. package/dist/agent-registry.d.ts +0 -27
  65. package/dist/agent-registry.js +0 -153
  66. package/dist/db.d.ts +0 -5
  67. package/dist/db.js +0 -220
  68. package/dist/helpers.d.ts +0 -21
  69. package/dist/helpers.js +0 -27
  70. package/dist/resources.d.ts +0 -2
  71. package/dist/resources.js +0 -89
  72. package/dist/sse.d.ts +0 -10
  73. package/dist/sse.js +0 -766
  74. package/dist/tmux-bridge.d.ts +0 -12
  75. package/dist/tmux-bridge.js +0 -157
  76. package/dist/tools/activity.d.ts +0 -39
  77. package/dist/tools/activity.js +0 -152
  78. package/dist/tools/agent-inbox.d.ts +0 -12
  79. package/dist/tools/agent-inbox.js +0 -257
  80. package/dist/tools/agent.d.ts +0 -14
  81. package/dist/tools/agent.js +0 -168
  82. package/dist/tools/analytics.d.ts +0 -21
  83. package/dist/tools/analytics.js +0 -191
  84. package/dist/tools/checkpoint.d.ts +0 -27
  85. package/dist/tools/checkpoint.js +0 -105
  86. package/dist/tools/notifications.d.ts +0 -31
  87. package/dist/tools/notifications.js +0 -59
  88. package/dist/tools/projects.d.ts +0 -55
  89. package/dist/tools/projects.js +0 -112
  90. package/dist/tools/settings.d.ts +0 -19
  91. package/dist/tools/settings.js +0 -73
  92. package/dist/tools/tasks.d.ts +0 -105
  93. package/dist/tools/tasks.js +0 -403
  94. package/dist/tools/terminal.d.ts +0 -4
  95. package/dist/tools/terminal.js +0 -98
  96. package/dist/tools/timer.d.ts +0 -37
  97. package/dist/tools/timer.js +0 -154
  98. package/dist/types.d.ts +0 -82
  99. package/dist/types.js +0 -30
@@ -0,0 +1,269 @@
1
+ /**
2
+ * The agent's connection to TaskFlow: register a session, prove liveness, and
3
+ * keep doing both for as long as the process lives.
4
+ *
5
+ * ## Why this is its own module
6
+ *
7
+ * This ran as step one of `startMirrorForThisAgent`'s `start(pane)` callback,
8
+ * behind `mirror.ts`'s "no tmux pane, nothing to mirror" early return. A
9
+ * capability (presence) was smuggled inside a feature (terminal mirroring), so
10
+ * the feature's precondition became the capability's: an agent outside tmux
11
+ * never registered, never heartbeat, and never appeared in the dashboard.
12
+ *
13
+ * Mirroring is now layered on top of this — `onSession` hands the caller a live
14
+ * session to attach the event stream and the pane mirror to.
15
+ *
16
+ * Retries are UNBOUNDED. The old startup gave up after 8 attempts (~2 minutes)
17
+ * and stayed dead for the life of the process, which is indistinguishable from
18
+ * the bug this module fixes: starting the MCP before the backend is a normal
19
+ * ordering, not an error.
20
+ */
21
+ import { TaskflowApiError, TaskflowClient } from "./client.js";
22
+ import { sessionIdentifier } from "./session-identifier.js";
23
+ import { hostname } from "node:os";
24
+ /** Backoff, matching `events.ts`'s reconnect policy exactly. */
25
+ const RETRY_BASE_MS = 1_000;
26
+ const RETRY_MAX_MS = 30_000;
27
+ /** 30s beats inside a 90s liveness window — a three-way contract with the
28
+ * backend's AGENT_HEARTBEAT_WINDOW_SECS and the frontend's ..._MS. */
29
+ const HEARTBEAT_MS = 30_000;
30
+ let status = { state: "starting", attempts: 0 };
31
+ /**
32
+ * Which connection the module-level `status` describes.
33
+ *
34
+ * There can be more than one live connection in a process — `select_profile`
35
+ * starts a second one after a human picks a profile — and a single shared
36
+ * variable let them cross-talk: the superseded connection's `stop()` would
37
+ * flip the LIVE connection's row to `stopped`, so `whoami` reported an offline
38
+ * agent that was in fact heartbeating. The newest connection owns the status;
39
+ * every write from an older one is dropped.
40
+ */
41
+ let statusOwner;
42
+ /** The current connection status, for `whoami` to report. */
43
+ export function getConnectionStatus() {
44
+ return { ...status };
45
+ }
46
+ /** Record that we are deliberately NOT connecting until a human picks. */
47
+ export function setNeedsProfile(detail) {
48
+ statusOwner = undefined;
49
+ status = { state: "needs_profile", detail, attempts: 0 };
50
+ }
51
+ /** Test seam — reset module-level status between cases. */
52
+ export function resetConnectionStatus() {
53
+ statusOwner = undefined;
54
+ status = { state: "starting", attempts: 0 };
55
+ }
56
+ /**
57
+ * The first line of why something failed, for ANY thrown value.
58
+ *
59
+ * Nothing guarantees a rejection carries an `Error`: `fetch` internals reject
60
+ * with `{code:"ECONNRESET"}`, libraries throw strings, and a bare
61
+ * `Promise.reject()` carries `undefined`. `(err as Error).message.split(...)`
62
+ * on those throws a TypeError from INSIDE the catch block, which escapes the
63
+ * handler that was supposed to contain it — the one thing this module must
64
+ * never do.
65
+ */
66
+ function reason(err) {
67
+ try {
68
+ const raw = err instanceof Error && typeof err.message === "string" ? err.message : String(err);
69
+ return raw.split("\n")[0] || "unknown";
70
+ }
71
+ catch {
72
+ return "unknown";
73
+ }
74
+ }
75
+ const defaultSleep = (ms) => new Promise((resolve) => {
76
+ const t = setTimeout(resolve, ms);
77
+ // Never hold the process open for a retry.
78
+ t.unref?.();
79
+ });
80
+ /**
81
+ * Connect and stay connected. Never throws and never rejects: the agent must
82
+ * keep working whether or not the dashboard is reachable.
83
+ */
84
+ export function startConnection(options) {
85
+ const { profile, pane, log: rawLog = () => { }, sleep = defaultSleep, heartbeatMs = HEARTBEAT_MS, autoHeartbeat = true, createClient = (p) => new TaskflowClient({ server: p.server, key: p.key }), } = options;
86
+ /** Diagnostics are never worth the connection: an injected `log` that throws
87
+ * (a closed stream, a broken transport) must not unwind a catch block. */
88
+ const log = (line) => {
89
+ try {
90
+ rawLog(line);
91
+ }
92
+ catch {
93
+ // Nowhere left to report it — stdout belongs to the MCP transport.
94
+ }
95
+ };
96
+ let stopped = false;
97
+ let session;
98
+ // Starting a connection makes it the one `whoami` describes; the previous
99
+ // one keeps running but can no longer write to the shared status.
100
+ const token = {};
101
+ statusOwner = token;
102
+ status = { state: "starting", attempts: 0, profile: profile.profileName };
103
+ /**
104
+ * Write the shared status — a no-op once this connection is superseded.
105
+ *
106
+ * The profile is stamped HERE rather than at each call site: every published
107
+ * status describes this connection, so there is no state in which the field
108
+ * could correctly be omitted, and a new publish site cannot forget it.
109
+ */
110
+ const publish = (next) => {
111
+ if (statusOwner !== token)
112
+ return;
113
+ status = { ...next, profile: profile.profileName };
114
+ };
115
+ /**
116
+ * Built on first use, INSIDE the retry loop's try — never in the function
117
+ * body. `new TaskflowClient(...)` throws on a profile with no `server` and
118
+ * on a runtime with no `fetch`, and a throw out here would escape
119
+ * `startConnection` itself: the caller would get no handle, no `settled` to
120
+ * catch on, and — since Task 6 calls this on the startup path — a dead MCP
121
+ * server at boot. Built once and reused so a retry does not leak clients.
122
+ */
123
+ let client;
124
+ let identifier;
125
+ const clientOf = () => (client ??= createClient(profile));
126
+ const identifierOf = () => (identifier ??= sessionIdentifier({
127
+ pane,
128
+ profileName: profile.profileName,
129
+ project: profile.project,
130
+ agentId: profile.agentId,
131
+ configPath: profile.configPath,
132
+ }));
133
+ const register = async () => {
134
+ const row = await clientOf().registerSession({
135
+ session_identifier: identifierOf(),
136
+ host: hostname(),
137
+ pid: process.pid,
138
+ cwd: process.cwd(),
139
+ transport: pane ? "tmux" : "mcp",
140
+ });
141
+ return row.id;
142
+ };
143
+ /** One heartbeat tick. A dead session is re-registered; anything else waits. */
144
+ const beat = async () => {
145
+ if (stopped || session === undefined)
146
+ return;
147
+ try {
148
+ await clientOf().heartbeat(session);
149
+ }
150
+ catch (err) {
151
+ // 404: the row is gone (backend restarted with a fresh DB, or it was
152
+ // swept). 401: the credential was rejected mid-flight. Both are fixed by
153
+ // registering again — it is idempotent per identifier and re-adopts the
154
+ // SAME row id, so the mirror's session number stays valid.
155
+ const status_ = err instanceof TaskflowApiError ? err.status : 0;
156
+ if (status_ === 404 || status_ === 401) {
157
+ try {
158
+ session = await register();
159
+ // `stop()` may have landed while the re-register request was in
160
+ // flight — mirrors the same check in `run()`. Without it, a
161
+ // torn-down connection's `beat()` can resolve after `stop()` and
162
+ // flip a correctly-`stopped` status back to a lying `active`.
163
+ if (stopped)
164
+ return;
165
+ // Built fresh, not spread from `status`: a spread-plus-`detail:
166
+ // undefined` creates an OWN `detail` key whose value is
167
+ // `undefined`, so `"detail" in status` reads true even though
168
+ // there is nothing to report.
169
+ publish({ state: "active", attempts: status.attempts, session });
170
+ log(`session re-registered as ${session}`);
171
+ }
172
+ catch (reErr) {
173
+ // The next tick tries again; a backend mid-restart is expected. But
174
+ // do NOT keep reporting a confident `active` against a session id
175
+ // the backend just 404'd — that lie would stand for a full 30s beat.
176
+ const detail = `session ${session} is gone; re-register failed (${reason(reErr)})`;
177
+ publish({ ...status, state: "retrying", detail });
178
+ log(detail);
179
+ }
180
+ return;
181
+ }
182
+ // Transient (network, 5xx): the next tick retries. Re-registering here
183
+ // would hammer a struggling backend with writes instead of cheap beats.
184
+ log(`heartbeat failed (${reason(err)})`);
185
+ }
186
+ };
187
+ const heartbeatLoop = async () => {
188
+ while (!stopped) {
189
+ try {
190
+ await sleep(heartbeatMs);
191
+ }
192
+ catch {
193
+ return; // test sleep budget exhausted, or the timer was torn down
194
+ }
195
+ try {
196
+ await beat();
197
+ }
198
+ catch (err) {
199
+ // `beat` contains its own failures, but a defect in that containment
200
+ // must cost one tick, not the whole loop: an escape here lands on an
201
+ // unhandled rejection and the agent goes silently offline forever.
202
+ // Logged (via the guarded `log`, never the raw callback) so a
203
+ // regression in that containment leaves a trace instead of vanishing.
204
+ log(`heartbeat loop caught an escape from beat() (${reason(err)})`);
205
+ }
206
+ }
207
+ };
208
+ const run = async () => {
209
+ for (let attempt = 1; !stopped; attempt += 1) {
210
+ let live;
211
+ try {
212
+ live = await register();
213
+ }
214
+ catch (err) {
215
+ const detail = reason(err);
216
+ publish({ state: "retrying", attempts: attempt, detail });
217
+ // Grow, cap, jitter — so a backend coming back up is not hit by every
218
+ // client at once.
219
+ const ceiling = Math.min(RETRY_BASE_MS * 2 ** (attempt - 1), RETRY_MAX_MS);
220
+ const delay = Math.round(ceiling / 2 + Math.random() * (ceiling / 2));
221
+ log(`not connected yet (${detail}) — retrying in ${delay}ms`);
222
+ try {
223
+ await sleep(delay);
224
+ }
225
+ catch {
226
+ return; // test sleep budget exhausted
227
+ }
228
+ continue;
229
+ }
230
+ // `stop()` may have landed while the register request was in flight.
231
+ // Without this, a torn-down connection reports itself active and hands
232
+ // the caller a session to attach a mirror and an event stream to.
233
+ if (stopped)
234
+ return;
235
+ session = live;
236
+ publish({ state: "active", attempts: attempt, session });
237
+ if (attempt > 1)
238
+ log(`connected after ${attempt} attempts`);
239
+ if (autoHeartbeat)
240
+ void heartbeatLoop();
241
+ // Deliberately OUTSIDE the retry loop and in its own try/catch. The
242
+ // caller attaches the pane mirror and the SSE stream here, and attaching
243
+ // genuinely fails in practice. That is a failed mirror, not a failed
244
+ // connection: the session IS registered and IS heartbeating, so it stays
245
+ // `active` and must never be re-registered — inside the loop, one
246
+ // rejection became an unbounded register storm against the backend.
247
+ try {
248
+ await options.onSession?.({ client: clientOf(), session, profile, pane });
249
+ }
250
+ catch (err) {
251
+ log(`session attach failed (${reason(err)}) — connection stays active`);
252
+ }
253
+ return;
254
+ }
255
+ };
256
+ // Last-resort net. Every failure path inside `run` is already contained; this
257
+ // exists so that a future one that is not cannot become an unhandled
258
+ // rejection in the MCP process, which is what this module's contract forbids.
259
+ const settled = run().catch(() => { });
260
+ return {
261
+ settled,
262
+ beat,
263
+ stop: () => {
264
+ stopped = true;
265
+ publish({ ...status, state: "stopped" });
266
+ },
267
+ };
268
+ }
269
+ //# sourceMappingURL=connect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connect.js","sourceRoot":"","sources":["../src/connect.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * `taskflow-mcp --check` — verify the whole agent chain from a terminal.
3
+ *
4
+ * The MCP tools can only be called by a model through an MCP client, so there is
5
+ * otherwise no way for a human to answer "is this thing actually wired up?"
6
+ * without hand-rolling curl. This walks the same path the server does — find the
7
+ * config, resolve each profile, authenticate against the backend — and reports
8
+ * where it breaks, with the fix.
9
+ *
10
+ * Writes to stdout (this mode is not the MCP transport) and exits non-zero on
11
+ * failure so it can gate a script.
12
+ */
13
+ export interface DoctorOptions {
14
+ configPath?: string | undefined;
15
+ startDir?: string | undefined;
16
+ env?: NodeJS.ProcessEnv;
17
+ /** Injected in tests; defaults to writing a line to stdout. */
18
+ log?: (line: string) => void;
19
+ }
20
+ /**
21
+ * Run every check. Returns a process exit code: 0 when the agent can
22
+ * authenticate as at least one profile, 1 otherwise.
23
+ */
24
+ export declare function runDoctor(options?: DoctorOptions): Promise<number>;
package/dist/doctor.js ADDED
@@ -0,0 +1,120 @@
1
+ /**
2
+ * `taskflow-mcp --check` — verify the whole agent chain from a terminal.
3
+ *
4
+ * The MCP tools can only be called by a model through an MCP client, so there is
5
+ * otherwise no way for a human to answer "is this thing actually wired up?"
6
+ * without hand-rolling curl. This walks the same path the server does — find the
7
+ * config, resolve each profile, authenticate against the backend — and reports
8
+ * where it breaks, with the fix.
9
+ *
10
+ * Writes to stdout (this mode is not the MCP transport) and exits non-zero on
11
+ * failure so it can gate a script.
12
+ */
13
+ import { TaskflowClient, TaskflowApiError } from "./client.js";
14
+ import { findConfigPath, loadConfigFile, resolveProfile, } from "./config.js";
15
+ const PASS = "PASS";
16
+ const FAIL = "FAIL";
17
+ /**
18
+ * Run every check. Returns a process exit code: 0 when the agent can
19
+ * authenticate as at least one profile, 1 otherwise.
20
+ */
21
+ export async function runDoctor(options = {}) {
22
+ const log = options.log ?? ((line) => process.stdout.write(`${line}\n`));
23
+ // 1. Locate the credential file.
24
+ let configPath;
25
+ try {
26
+ configPath = findConfigPath({
27
+ configPath: options.configPath,
28
+ startDir: options.startDir,
29
+ env: options.env,
30
+ });
31
+ }
32
+ catch (err) {
33
+ log(`${FAIL} .taskflow.json not found`);
34
+ log(` ${err.message}`);
35
+ log("");
36
+ log(" Fix: link an agent on the dashboard's API Base page, then save the");
37
+ log(" snippet as .taskflow.json in your repo root.");
38
+ return 1;
39
+ }
40
+ log(`${PASS} config found — ${configPath}`);
41
+ // 2. Parse + validate it.
42
+ let config;
43
+ try {
44
+ config = loadConfigFile(configPath);
45
+ }
46
+ catch (err) {
47
+ log(`${FAIL} config is not valid`);
48
+ log(` ${err.message}`);
49
+ return 1;
50
+ }
51
+ const profileNames = Object.keys(config.profiles);
52
+ log(`${PASS} config valid — server ${config.server}, project ${config.project}`);
53
+ log(` profiles: ${profileNames.join(", ")} (default: ${config.default_profile ?? "main"})`);
54
+ // A frontend origin here is the classic misconfiguration: the dev server
55
+ // proxies /api to the backend, so it appears to work until the frontend is
56
+ // stopped, at which point the agent dies for no obvious reason.
57
+ if (/:(517[0-9]|300[0-9]|808[0-9])$/.test(config.server) && !/:8000$/.test(config.server)) {
58
+ log(` note: ${config.server} looks like a frontend dev server. It must be the`);
59
+ log(" BACKEND origin — an agent runs headless and must not depend on the UI.");
60
+ }
61
+ // 3. Authenticate each profile against the live backend. Readiness hinges on
62
+ // the DEFAULT profile — that is the one tools use when no profile is named, so
63
+ // a working "reviewer" next to a broken "main" is not a ready setup.
64
+ const defaultName = config.default_profile ?? "main";
65
+ const failed = [];
66
+ let defaultOk = false;
67
+ for (const name of profileNames) {
68
+ let resolved;
69
+ try {
70
+ resolved = resolveProfile(config, { profile: name, env: options.env, configPath });
71
+ }
72
+ catch (err) {
73
+ log(`${FAIL} profile "${name}" — ${err.message}`);
74
+ failed.push(name);
75
+ continue;
76
+ }
77
+ const client = new TaskflowClient({ server: resolved.server, key: resolved.key });
78
+ try {
79
+ const who = await client.whoami();
80
+ if (name === defaultName)
81
+ defaultOk = true;
82
+ log(`${PASS} profile "${name}" authenticated — agent #${who.agent_id} ` +
83
+ `"${who.display_name}", project ${who.project}, status ${who.status}`);
84
+ }
85
+ catch (err) {
86
+ log(`${FAIL} profile "${name}" could not authenticate`);
87
+ failed.push(name);
88
+ const message = err.message;
89
+ log(` ${message}`);
90
+ // The client wraps a network failure as a TaskflowApiError with status 0,
91
+ // so "could not connect" must be matched on the message, not by falling
92
+ // through to a non-API error branch — there isn't one.
93
+ const status = err instanceof TaskflowApiError ? err.status : -1;
94
+ if (status === 401 || status === 403) {
95
+ log(" The key is wrong, revoked, or belongs to another project.");
96
+ log(" Fix: re-link the agent on the API Base page for a fresh key.");
97
+ }
98
+ else if (status === 0 || /ECONNREFUSED|fetch failed|ENOTFOUND|network/i.test(message)) {
99
+ log(` Nothing is listening at ${resolved.server}.`);
100
+ log(' Fix: start the backend, or correct the "server" field.');
101
+ }
102
+ }
103
+ }
104
+ log("");
105
+ if (defaultOk && failed.length === 0) {
106
+ log("Ready. The agent can authenticate — MCP tools will work in Claude Code.");
107
+ }
108
+ else if (defaultOk) {
109
+ log(`Mostly ready. The default profile "${defaultName}" works, so tools will run, ` +
110
+ `but ${failed.length === 1 ? "profile" : "profiles"} ${failed
111
+ .map((f) => `"${f}"`)
112
+ .join(", ")} failed and cannot be used.`);
113
+ }
114
+ else {
115
+ log(`Not ready. The default profile "${defaultName}" could not authenticate; ` +
116
+ "see the failures above.");
117
+ }
118
+ return defaultOk ? 0 : 1;
119
+ }
120
+ //# sourceMappingURL=doctor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor.js","sourceRoot":"","sources":["../src/doctor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Listen to the agent's server-sent event stream so messages reach the terminal
3
+ * the instant they are sent.
4
+ *
5
+ * This is v1's tmux-bridge idea (`task_flow/mcp-server/src/tmux-bridge.ts`):
6
+ * hold a stream open for the life of the session and inject on arrival, instead
7
+ * of polling. Polling made delivery as slow as the interval and cost a request
8
+ * per tick even in silence.
9
+ *
10
+ * It cannot use `EventSource`: agents authenticate with `Authorization: Agent
11
+ * <key>`, and EventSource cannot set headers. Node's fetch can, so the SSE frame
12
+ * parsing is done here — it is a small, well-specified format.
13
+ */
14
+ import { TaskflowClient } from "./client.js";
15
+ export interface AgentMessageEvent {
16
+ id: number;
17
+ channel: number;
18
+ sender_kind: string;
19
+ sender_label: string;
20
+ body_markdown: string;
21
+ sender_agent: number | null;
22
+ /** Context ids carried on the resolved read row (absent on the id-only wire
23
+ * event). Surfaced in the pane notice so the agent can query/act on the
24
+ * right project/task/person without a round trip — see `formatIncoming`. */
25
+ project?: number | null;
26
+ task?: number | null;
27
+ sender_user?: number | null;
28
+ /** #29: a directed group message names the one agent whose pane should get it;
29
+ * null broadcasts to every agent on the channel (the default). Kept for
30
+ * back-compat with rows written before `targets`; `targets` is authoritative
31
+ * when present. */
32
+ target_agent?: number | null;
33
+ /** #29: the full directed-target set — agents (pane delivery) and users
34
+ * (mention only). Empty/absent broadcasts to every agent, like a null
35
+ * `target_agent`. Supersedes `target_agent` when present. */
36
+ targets?: MessageTarget[] | null;
37
+ }
38
+ /** #29: one directed target of a message. `kind` is "agent" or "user"; only
39
+ * agent targets gate pane delivery (users are mention/attribution). */
40
+ export interface MessageTarget {
41
+ kind: string;
42
+ id: number;
43
+ }
44
+ /** A terminal key the dashboard sent, projected on the realtime event. `agent`
45
+ * is the target pane's agent; `keys` is one tmux key name. */
46
+ export interface TerminalKeyEvent {
47
+ agent: number;
48
+ keys: string;
49
+ }
50
+ export interface EventStreamOptions {
51
+ server: string;
52
+ key: string;
53
+ /** Called for each message created — or edited (#107) — in this agent's
54
+ * project. `action` says which; absent means "created" (older callers). */
55
+ onMessage: (message: AgentMessageEvent, action?: "created" | "updated") => void | Promise<void>;
56
+ /** Called when a human answers a question this agent is blocked on. */
57
+ onPromptAnswered?: (prompt: PromptEvent) => void | Promise<void>;
58
+ /** Called when a human sends a terminal key from the dashboard. Broadcast to
59
+ * the whole project, so `agent` says which pane it is for — the handler must
60
+ * ignore keys addressed to other agents. */
61
+ onTerminalKey?: (input: TerminalKeyEvent) => void | Promise<void>;
62
+ /** Called each time the stream RE-connects (not the first connect). The live
63
+ * push is at-most-once, so the handler catches up on messages that arrived
64
+ * while the stream was down. */
65
+ onReconnect?: () => void | Promise<void>;
66
+ log?: (line: string) => void;
67
+ /** Injected in tests. */
68
+ fetchImpl?: typeof fetch;
69
+ }
70
+ export interface EventStreamHandle {
71
+ stop: () => void;
72
+ /**
73
+ * Drop the current connection and reconnect now.
74
+ *
75
+ * The idle watchdog is a backstop measured in tens of seconds; when something
76
+ * else in this process has just proven the backend is unreachable (a failed
77
+ * heartbeat, say), there is no reason to wait for it.
78
+ */
79
+ reconnectNow: () => void;
80
+ }
81
+ /**
82
+ * Hold the stream open, reconnecting with backoff.
83
+ *
84
+ * Never rejects: a stream that cannot connect retries quietly forever, because
85
+ * the agent must keep working whether or not the dashboard is reachable.
86
+ */
87
+ export declare function startAgentEventStream(options: EventStreamOptions): EventStreamHandle;
88
+ /** Pull the `data:` lines out of one SSE frame and dispatch a message event. */
89
+ export declare function handleFrame(frame: string, options: Pick<EventStreamOptions, "onMessage" | "onPromptAnswered" | "onTerminalKey">): void;
90
+ /**
91
+ * Whether a message should be delivered to THIS agent's terminal.
92
+ *
93
+ * Every message the agent can see is delivered EXCEPT its own — an agent must
94
+ * stay aware of all channel traffic so it can chat / add information (#40 review).
95
+ * Whether it may ACT on the message is a SEPARATE signal (`canActOnMessage`)
96
+ * surfaced as guidance in the notice, NOT a delivery gate. Its own messages are
97
+ * still skipped: echoing them back reads as a new instruction and could loop the
98
+ * agent against itself.
99
+ */
100
+ export declare function shouldDeliver(message: AgentMessageEvent, selfAgentId: number): boolean;
101
+ /**
102
+ * Whether THIS agent is a TARGET of the message and may act on it — build,
103
+ * execute, take ownership. A non-target still RECEIVES the message (to reply or
104
+ * add information) but must not act; `formatIncoming` says so.
105
+ *
106
+ * - An empty/absent target set is a broadcast: everyone may act.
107
+ * - Otherwise the agent may act only if it is one of the AGENT targets. A
108
+ * message tagged only at users leaves every agent a non-target.
109
+ */
110
+ export declare function canActOnMessage(message: AgentMessageEvent, selfAgentId: number): boolean;
111
+ /** Format a delivered message the way v1 did: who it is from, then the text. */
112
+ /** The attachment fields a notice needs. Matches the agent read projection. */
113
+ export interface NoticeAttachment {
114
+ name: string;
115
+ size_bytes: number;
116
+ url: string;
117
+ }
118
+ export declare function formatIncoming(message: AgentMessageEvent, attachments?: NoticeAttachment[], selfAgentId?: number, edited?: boolean): string;
119
+ export type { TaskflowClient };
120
+ /**
121
+ * The keystrokes that answer a prompt, as verified against a live Claude Code
122
+ * session — not inferred from the UI's appearance.
123
+ *
124
+ * SINGLE-select: the number HIGHLIGHTS the option; Enter submits it.
125
+ * press "2" -> option 2 highlighted, still waiting
126
+ * press Enter -> "You selected: Green"
127
+ *
128
+ * This one was wrong here until 2026-07-21. The comment claimed the number both
129
+ * selected and submitted, so the agent was left sitting on a prompt the
130
+ * dashboard already showed as answered — the worst shape of failure, because
131
+ * the human sees a completed action and the agent sees nothing.
132
+ *
133
+ * MULTI-select: numbers TOGGLE checkboxes, then Right opens the review screen
134
+ * ("Ready to submit your answers?" — 1. Submit / 2. Cancel). This function only
135
+ * REACHES that screen; the submit key is appended once by keystrokesForPrompt,
136
+ * because a multi-question set lands on the identical screen. Verified live
137
+ * 2026-07-21: a single multi-select question sat on that review screen because
138
+ * nothing pressed the final key.
139
+ * press "1" -> [✔] Lint
140
+ * press "3" -> [✔] Tests
141
+ * press Right -> review screen (submit handled by the caller)
142
+ *
143
+ * Getting this wrong is not a cosmetic bug: sending a single digit to a
144
+ * multi-select ticks one box and leaves the agent still waiting, and sending
145
+ * Enter instead of Right submits an empty answer.
146
+ */
147
+ export declare function answerKeystrokes(choices: number[], kind: string): string[];
148
+ export interface PromptEvent {
149
+ id: number;
150
+ session: number;
151
+ question: string;
152
+ kind: string;
153
+ status: string;
154
+ answer: number | null;
155
+ answer_json: string | null;
156
+ /** Set when a HUMAN answered or cancelled it; null when the agent cleared it. */
157
+ answered_by?: number | null;
158
+ /**
159
+ * The options, as the hook wrote them. Carried on the realtime projection
160
+ * (`PROMPT_FIELDS` in backend/src/realtime.rs) because a multi-question prompt
161
+ * cannot be turned into keystrokes without knowing each question's kind — a
162
+ * set can mix single and multi.
163
+ */
164
+ options_json?: string;
165
+ /**
166
+ * Free-text "Other" answers as JSON, one entry per question (`null` where the
167
+ * question has no Other pick). Carried on the realtime projection so the MCP
168
+ * can type the value into the terminal's "Type something" field.
169
+ */
170
+ answer_text_json?: string | null;
171
+ }
172
+ /** The numbers a human chose, from whichever field carries them. */
173
+ export declare function chosenNumbers(prompt: PromptEvent): number[];
174
+ /**
175
+ * Whether this event is a human decision this agent should act on.
176
+ *
177
+ * `answered` and `cancelled` are BOTH actionable: cancel is the other button on
178
+ * the review screen, and reaching it means replaying every answer first, so it
179
+ * is just as much a keystroke sequence as submit.
180
+ *
181
+ * `answered_by` is what separates a human cancel from the agent clearing its own
182
+ * prompt — it times out, or someone answers in the terminal directly, and the
183
+ * row is marked cancelled with nobody attributed. Firing keys for that would
184
+ * type digits at a screen the agent has already left.
185
+ */
186
+ export declare function isAnsweredPrompt(row: unknown): row is PromptEvent;