@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
package/dist/mint.js ADDED
@@ -0,0 +1,135 @@
1
+ /**
2
+ * `taskflow-mcp --mint <name>` — create a NEW agent identity and record it as
3
+ * a profile in `.taskflow.json`.
4
+ *
5
+ * ## Why this is a CLI and not an MCP tool
6
+ *
7
+ * `POST /api/taskflow/agents/link` is gated on `RequireAuth` plus an active
8
+ * project membership, and it stamps the linking human onto the agent. An agent
9
+ * authenticates with `Authorization: Agent <key>` and would simply be refused.
10
+ *
11
+ * That gate is correct and this command deliberately does not work around it:
12
+ * an agent able to mint sibling agents could grow an unbounded roster that no
13
+ * human vouched for. Creating an identity is a human act, so a human runs this.
14
+ *
15
+ * ## What it is for
16
+ *
17
+ * Identity is `project + profile`. Two terminals sharing the `main` profile are
18
+ * ONE agent — same roster entry, and, more importantly, one shared DM inbox and
19
+ * one shared read cursor. Giving the second terminal its own profile is what
20
+ * makes them distinguishable.
21
+ */
22
+ import { readFileSync, writeFileSync } from "node:fs";
23
+ import { findConfigPath, taskflowConfigSchema } from "./config.js";
24
+ /** A clear, user-facing failure (never a raw stack). */
25
+ export class MintError extends Error {
26
+ constructor(message) {
27
+ super(message);
28
+ this.name = "MintError";
29
+ }
30
+ }
31
+ /**
32
+ * Parse `--mint <name> [--display-name <s>] [--token <t>]`.
33
+ *
34
+ * The token falls back to `TASKFLOW_USER_TOKEN`, but an explicit `--token`
35
+ * wins — a one-off mint against another account must not silently pick up
36
+ * whatever is exported in the shell.
37
+ */
38
+ export function parseMintArgs(argv, env = process.env) {
39
+ const valueFor = (flag) => {
40
+ const i = argv.indexOf(flag);
41
+ if (i === -1)
42
+ return undefined;
43
+ const next = argv[i + 1];
44
+ // A flag consuming the next flag as its value is how `--mint --token t`
45
+ // would otherwise mint a profile literally named "--token".
46
+ return next && !next.startsWith("--") ? next : undefined;
47
+ };
48
+ const name = valueFor("--mint");
49
+ if (!name) {
50
+ throw new MintError("--mint needs a profile name, e.g. `--mint bear`.");
51
+ }
52
+ return {
53
+ name,
54
+ displayName: valueFor("--display-name") ?? `Claude (${name})`,
55
+ token: valueFor("--token") ?? env.TASKFLOW_USER_TOKEN,
56
+ };
57
+ }
58
+ /**
59
+ * Return a copy of `config` with `profile` recorded under `name`.
60
+ *
61
+ * Pure, and deliberately non-destructive in three ways:
62
+ * - an existing profile is never overwritten (its key is unrecoverable, and it
63
+ * is what a running terminal authenticates with)
64
+ * - `default_profile` never moves, so existing terminals keep their identity
65
+ * - every other field is carried through untouched
66
+ */
67
+ export function addProfile(config, name, profile) {
68
+ const key = name.trim();
69
+ if (!key) {
70
+ throw new MintError("A profile name must not be blank.");
71
+ }
72
+ if (config.profiles[key]) {
73
+ throw new MintError(`Profile "${key}" already exists in .taskflow.json. ` +
74
+ `Pick another name — overwriting it would discard a key that cannot be recovered.`);
75
+ }
76
+ return { ...config, profiles: { ...config.profiles, [key]: profile } };
77
+ }
78
+ /** The line the operator runs to start the other terminal as the new identity. */
79
+ export function exportHint(name) {
80
+ return `export TASKFLOW_PROFILE=${name}`;
81
+ }
82
+ /** Mint against the server and return the profile block it hands back. */
83
+ export async function requestMint(server, project, args, token, doFetch = fetch) {
84
+ const res = await doFetch(`${server.replace(/\/$/, "")}/api/taskflow/agents/link`, {
85
+ method: "POST",
86
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
87
+ body: JSON.stringify({ project, display_name: args.displayName, profile: args.name }),
88
+ });
89
+ if (res.status === 401) {
90
+ throw new MintError("The server rejected that token (401). It must be a USER token, not an agent key.");
91
+ }
92
+ if (res.status === 403) {
93
+ throw new MintError(`That account is not an active member of project ${project} (403).`);
94
+ }
95
+ if (!res.ok) {
96
+ throw new MintError(`Mint failed: ${res.status} ${res.statusText}`);
97
+ }
98
+ const body = (await res.json());
99
+ // The raw key exists in this response and nowhere else — the server stores
100
+ // only a hash. A malformed body here means the key is already lost, so say so
101
+ // rather than writing a profile that cannot authenticate.
102
+ if (!body.taskflow_profile?.key) {
103
+ throw new MintError("The server's response carried no key; the minted agent is unusable. Nothing was written.");
104
+ }
105
+ return body.taskflow_profile;
106
+ }
107
+ /**
108
+ * Run the command. Returns the lines to print.
109
+ *
110
+ * The write is last: a failed mint must not leave a half-edited config, and a
111
+ * successful mint must not lose its key to a formatting error.
112
+ */
113
+ export async function runMint(argv, env = process.env) {
114
+ const args = parseMintArgs(argv, env);
115
+ if (!args.token) {
116
+ throw new MintError("No user token. Pass --token <t> or set TASKFLOW_USER_TOKEN.\n" +
117
+ "Minting an agent is a human action, so this needs YOUR login token, not an agent key.");
118
+ }
119
+ const configPath = findConfigPath({ env });
120
+ const config = taskflowConfigSchema.parse(JSON.parse(readFileSync(configPath, "utf8")));
121
+ // Fail on a duplicate name BEFORE minting, so a mistyped command cannot leave
122
+ // an orphan agent on the server that nothing references.
123
+ addProfile(config, args.name, { agent_id: 0, key: "probe" });
124
+ const profile = await requestMint(config.server, config.project, args, args.token);
125
+ const next = addProfile(config, args.name, profile);
126
+ writeFileSync(configPath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
127
+ return [
128
+ `minted agent ${profile.agent_id} — ${profile.display_name ?? args.displayName}`,
129
+ `wrote profile "${args.name}" to ${configPath}`,
130
+ "",
131
+ "run the other terminal with:",
132
+ ` ${exportHint(args.name)}`,
133
+ ];
134
+ }
135
+ //# sourceMappingURL=mint.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mint.js","sourceRoot":"","sources":["../src/mint.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Mirror lifecycle: start the terminal mirror, keep trying if the backend is not
3
+ * ready yet, and remember what happened so a human can ask.
4
+ *
5
+ * This module exists because of a real failure. The mirror's *running* loop is
6
+ * carefully resilient — `startMirrorLoop` swallows transient errors and
7
+ * reconnects the event stream — but its *startup* had no such protection. Setup
8
+ * calls `registerSession`, so a backend that was still booting when the MCP
9
+ * spawned threw once, was caught, logged to stderr, and mirroring was dead for
10
+ * the entire life of the process. The dashboard then showed a stale terminal
11
+ * with nothing anywhere saying why.
12
+ *
13
+ * Restarting the backend and reconnecting the MCP together is the normal way to
14
+ * pick up a change, which is exactly when the race is most likely.
15
+ */
16
+ export type MirrorState =
17
+ /** Not attempted yet. */
18
+ "starting"
19
+ /** Deliberately not mirroring — no tmux pane to mirror. Not an error. */
20
+ | "off"
21
+ /** Streaming. */
22
+ | "active"
23
+ /** Gave up after exhausting retries; `detail` carries the last reason. */
24
+ | "failed";
25
+ export interface MirrorStatus {
26
+ state: MirrorState;
27
+ /** Why, in the caller's words — always set for `off` and `failed`. */
28
+ detail?: string;
29
+ /** The tmux pane being mirrored, once known. */
30
+ pane?: string;
31
+ /** How many start attempts have been made. */
32
+ attempts: number;
33
+ }
34
+ /** The current mirror status, for `whoami` to report. */
35
+ export declare function getMirrorStatus(): MirrorStatus;
36
+ /**
37
+ * Publish the mirror's state from whoever is driving it — since Task 5 that is
38
+ * `runtime.ts`, not `startMirrorWithRetry`.
39
+ *
40
+ * Without this, every writer of `status` lived inside `startMirrorWithRetry`,
41
+ * which the startup path no longer calls: `whoami` reported a streaming mirror
42
+ * as `{state: "starting"}` forever, inverting the very problem this field
43
+ * exists to solve.
44
+ */
45
+ export declare function reportMirror(next: MirrorStatus): void;
46
+ /** Test seam — reset the module-level status between cases. */
47
+ export declare function resetMirrorStatus(): void;
48
+ export interface StartMirrorOptions {
49
+ /** Resolve the pane to mirror, or null when not running under tmux. */
50
+ detectPane: () => Promise<string | null>;
51
+ /** Do the actual setup (register, stream, loop). Throws if it cannot. */
52
+ start: (pane: string) => Promise<void>;
53
+ maxAttempts?: number;
54
+ baseMs?: number;
55
+ maxMs?: number;
56
+ sleep?: (ms: number) => Promise<void>;
57
+ log?: (line: string) => void;
58
+ }
59
+ /**
60
+ * Start the mirror, retrying with backoff until it takes or the attempts run
61
+ * out. Never throws: a mirror is a nice-to-have and must not be able to take
62
+ * the tool server down with it.
63
+ *
64
+ * No pane means no tmux, which is a permanent and legitimate condition — that
65
+ * returns immediately rather than retrying, so a non-tmux agent does not spend
66
+ * eight backoff rounds discovering the same thing.
67
+ */
68
+ export declare function startMirrorWithRetry(options: StartMirrorOptions): Promise<void>;
package/dist/mirror.js ADDED
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Mirror lifecycle: start the terminal mirror, keep trying if the backend is not
3
+ * ready yet, and remember what happened so a human can ask.
4
+ *
5
+ * This module exists because of a real failure. The mirror's *running* loop is
6
+ * carefully resilient — `startMirrorLoop` swallows transient errors and
7
+ * reconnects the event stream — but its *startup* had no such protection. Setup
8
+ * calls `registerSession`, so a backend that was still booting when the MCP
9
+ * spawned threw once, was caught, logged to stderr, and mirroring was dead for
10
+ * the entire life of the process. The dashboard then showed a stale terminal
11
+ * with nothing anywhere saying why.
12
+ *
13
+ * Restarting the backend and reconnecting the MCP together is the normal way to
14
+ * pick up a change, which is exactly when the race is most likely.
15
+ */
16
+ /** Backoff between startup attempts. Mirrors `events.ts`'s reconnect policy. */
17
+ const START_BASE_MS = 1_000;
18
+ const START_MAX_MS = 30_000;
19
+ const START_MAX_ATTEMPTS = 8;
20
+ let status = { state: "starting", attempts: 0 };
21
+ /** The current mirror status, for `whoami` to report. */
22
+ export function getMirrorStatus() {
23
+ return { ...status };
24
+ }
25
+ /**
26
+ * Publish the mirror's state from whoever is driving it — since Task 5 that is
27
+ * `runtime.ts`, not `startMirrorWithRetry`.
28
+ *
29
+ * Without this, every writer of `status` lived inside `startMirrorWithRetry`,
30
+ * which the startup path no longer calls: `whoami` reported a streaming mirror
31
+ * as `{state: "starting"}` forever, inverting the very problem this field
32
+ * exists to solve.
33
+ */
34
+ export function reportMirror(next) {
35
+ status = next;
36
+ }
37
+ /** Test seam — reset the module-level status between cases. */
38
+ export function resetMirrorStatus() {
39
+ status = { state: "starting", attempts: 0 };
40
+ }
41
+ const wait = (ms) => new Promise((resolve) => {
42
+ const t = setTimeout(resolve, ms);
43
+ // Never hold the process open for a retry.
44
+ t.unref?.();
45
+ });
46
+ /**
47
+ * Start the mirror, retrying with backoff until it takes or the attempts run
48
+ * out. Never throws: a mirror is a nice-to-have and must not be able to take
49
+ * the tool server down with it.
50
+ *
51
+ * No pane means no tmux, which is a permanent and legitimate condition — that
52
+ * returns immediately rather than retrying, so a non-tmux agent does not spend
53
+ * eight backoff rounds discovering the same thing.
54
+ */
55
+ export async function startMirrorWithRetry(options) {
56
+ const { detectPane, start, maxAttempts = START_MAX_ATTEMPTS, baseMs = START_BASE_MS, maxMs = START_MAX_MS, sleep = wait, log, } = options;
57
+ let pane;
58
+ try {
59
+ pane = await detectPane();
60
+ }
61
+ catch (err) {
62
+ status = {
63
+ state: "failed",
64
+ detail: err.message,
65
+ attempts: 0,
66
+ };
67
+ log?.(`terminal mirror unavailable (${err.message})`);
68
+ return;
69
+ }
70
+ if (!pane) {
71
+ status = {
72
+ state: "off",
73
+ detail: "not running inside tmux — nothing to mirror",
74
+ attempts: 0,
75
+ };
76
+ return;
77
+ }
78
+ let lastError = "";
79
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
80
+ try {
81
+ await start(pane);
82
+ status = { state: "active", pane, attempts: attempt };
83
+ if (attempt > 1)
84
+ log?.(`terminal mirror started after ${attempt} attempts`);
85
+ return;
86
+ }
87
+ catch (err) {
88
+ lastError = err.message.split("\n")[0] ?? String(err);
89
+ status = { state: "starting", pane, attempts: attempt, detail: lastError };
90
+ if (attempt === maxAttempts)
91
+ break;
92
+ // Same shape as the event stream's reconnect: grow, cap, and jitter so a
93
+ // backend coming back up is not hit by every client at once.
94
+ const ceiling = Math.min(baseMs * 2 ** (attempt - 1), maxMs);
95
+ const delay = Math.round(ceiling / 2 + Math.random() * (ceiling / 2));
96
+ log?.(`terminal mirror not up yet (${lastError}) — retrying in ${delay}ms`);
97
+ await sleep(delay);
98
+ }
99
+ }
100
+ status = { state: "failed", pane, detail: lastError, attempts: maxAttempts };
101
+ log?.(`terminal mirror gave up after ${maxAttempts} attempts (${lastError})`);
102
+ }
103
+ //# sourceMappingURL=mirror.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mirror.js","sourceRoot":"","sources":["../src/mirror.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * A serial queue for pane writes.
3
+ *
4
+ * ## Why this exists
5
+ *
6
+ * Every write to the agent's tmux pane — delivering a message, replaying a
7
+ * prompt answer, sending a terminal key — is a MULTI-STEP tmux sequence
8
+ * (`send-keys -l <text>` then `send-keys Enter`, or a paced run of keys). The
9
+ * event stream dispatches handlers fire-and-forget (`void onMessage(...)`), so
10
+ * two writes that land close together used to interleave: `typeA, typeB,
11
+ * EnterA, EnterB` — and the first Enter submits BOTH texts as one line while the
12
+ * second submits nothing. That is the "a message meant for someone else was
13
+ * typed into my pane and the next message's Enter submitted it" race.
14
+ *
15
+ * Routing every pane write through one serial queue makes each sequence
16
+ * atomic with respect to the others: a write runs to completion before the next
17
+ * begins, so type/Enter pairs can never cross.
18
+ */
19
+ /** Runs enqueued tasks one at a time, in call order. */
20
+ export interface SerialQueue {
21
+ /** Enqueue a task; resolves/rejects with the task's own result. */
22
+ (task: () => Promise<void>): Promise<void>;
23
+ }
24
+ /**
25
+ * Create a serial queue. Each task waits for the previous one to settle (success
26
+ * OR failure) before it starts, so a single failing write never wedges or
27
+ * reorders the queue. The caller still sees its own task's rejection.
28
+ */
29
+ export declare function createSerialQueue(): SerialQueue;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * A serial queue for pane writes.
3
+ *
4
+ * ## Why this exists
5
+ *
6
+ * Every write to the agent's tmux pane — delivering a message, replaying a
7
+ * prompt answer, sending a terminal key — is a MULTI-STEP tmux sequence
8
+ * (`send-keys -l <text>` then `send-keys Enter`, or a paced run of keys). The
9
+ * event stream dispatches handlers fire-and-forget (`void onMessage(...)`), so
10
+ * two writes that land close together used to interleave: `typeA, typeB,
11
+ * EnterA, EnterB` — and the first Enter submits BOTH texts as one line while the
12
+ * second submits nothing. That is the "a message meant for someone else was
13
+ * typed into my pane and the next message's Enter submitted it" race.
14
+ *
15
+ * Routing every pane write through one serial queue makes each sequence
16
+ * atomic with respect to the others: a write runs to completion before the next
17
+ * begins, so type/Enter pairs can never cross.
18
+ */
19
+ /**
20
+ * Create a serial queue. Each task waits for the previous one to settle (success
21
+ * OR failure) before it starts, so a single failing write never wedges or
22
+ * reorders the queue. The caller still sees its own task's rejection.
23
+ */
24
+ export function createSerialQueue() {
25
+ // The tail is a promise that settles when the last-enqueued task is done. It
26
+ // is kept deliberately error-swallowed so one rejection cannot break the chain
27
+ // for everything queued after it.
28
+ let tail = Promise.resolve();
29
+ return (task) => {
30
+ const run = tail.then(task, task);
31
+ tail = run.then(() => { }, () => { });
32
+ return run;
33
+ };
34
+ }
35
+ //# sourceMappingURL=pane-queue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pane-queue.js","sourceRoot":"","sources":["../src/pane-queue.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAQH;;;;GAIG;AACH,MAAM,UAAU,iBAAiB;IAC/B,6EAA6E;IAC7E,+EAA+E;IAC/E,kCAAkC;IAClC,IAAI,IAAI,GAAkB,OAAO,CAAC,OAAO,EAAE,CAAC;IAC5C,OAAO,CAAC,IAAI,EAAE,EAAE;QACd,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAClC,IAAI,GAAG,GAAG,CAAC,IAAI,CACb,GAAG,EAAE,GAAE,CAAC,EACR,GAAG,EAAE,GAAE,CAAC,CACT,CAAC;QACF,OAAO,GAAG,CAAC;IACb,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Reading a dashboard-answered prompt back into keystrokes.
3
+ *
4
+ * `AskUserQuestion` accepts SEVERAL questions in one call. The agent's terminal
5
+ * presents them one at a time — question 2's screen only exists once question 1
6
+ * is answered — so a whole set is stored as ONE prompt row and replayed in order
7
+ * only when every question has an answer. Replaying half a set would send the
8
+ * remaining digits at whatever screen the agent had moved on to.
9
+ *
10
+ * Two on-the-wire shapes are supported, because rows written before
11
+ * multi-question support are still in the table:
12
+ *
13
+ * legacy options_json = [{number, label}, ...] one question
14
+ * answer_json = [1, 3] one flat set
15
+ *
16
+ * set options_json = [{question, kind, options}] N questions
17
+ * answer_json = [[1], [2, 3]] one set per question
18
+ *
19
+ * A prompt that cannot be read yields NO keystrokes. An agent left waiting is
20
+ * recoverable; digits fired into the wrong screen are not.
21
+ */
22
+ export interface PromptOption {
23
+ number: number;
24
+ label: string;
25
+ description?: string;
26
+ isOther?: boolean;
27
+ }
28
+ export type KeyStep = {
29
+ key: string;
30
+ } | {
31
+ text: string;
32
+ };
33
+ export interface PromptQuestion {
34
+ question: string;
35
+ kind: string;
36
+ options: PromptOption[];
37
+ }
38
+ /**
39
+ * The questions a prompt is asking.
40
+ *
41
+ * `fallbackKind` and `fallbackQuestion` come from the row's own columns and are
42
+ * used only for the legacy shape, which stored the question text and kind there
43
+ * rather than inside the JSON.
44
+ */
45
+ export declare function parseQuestions(optionsJson: string, fallbackKind: string, fallbackQuestion: string): PromptQuestion[];
46
+ /**
47
+ * The chosen numbers, one set per question.
48
+ *
49
+ * `answer` is the row's single-answer column, kept as a fallback for rows
50
+ * answered before `answer_json` existed.
51
+ */
52
+ export declare function parseAnswerSets(answerJson: string | null, answer: number | null): number[][];
53
+ /** Whether every question in the set has at least one choice. */
54
+ export declare function isFullyAnswered(sets: number[][], questionCount: number): boolean;
55
+ /** What the human chose to do with an answered prompt. */
56
+ export type PromptIntent = "submit" | "cancel";
57
+ /**
58
+ * The full keystroke sequence for an answered prompt, or `[]` when it must not
59
+ * be replayed.
60
+ *
61
+ * Each question contributes its own sequence and they run back to back, in the
62
+ * order the questions were asked — the terminal advances to the next question as
63
+ * each is submitted. A set then confirms at the review screen.
64
+ *
65
+ * A single SINGLE-select question has no review screen (it submits as soon as it
66
+ * is answered), so nothing is appended and there is no safe "cancel" key. A
67
+ * single MULTI-select question DOES reach the review screen, so it gets the
68
+ * submit/cancel like a set does.
69
+ */
70
+ export declare function keystrokesForPrompt(optionsJson: string, kind: string, question: string, answerJson: string | null, answer: number | null, intent?: PromptIntent): string[];
71
+ /** The Other free-text value per question, parallel to the answer sets. */
72
+ export declare function parseAnswerTexts(answerTextJson: string | null, count: number): (string | null)[];
73
+ /**
74
+ * The full ordered steps for an answered prompt. Without any Other text this is
75
+ * `keystrokesForPrompt` wrapped as `{key}` steps. With Other text on a single
76
+ * multi-select question, it walks the arrow-based flow the new TUI uses (see the
77
+ * hypothesis in the test — verified live before this ships).
78
+ */
79
+ export declare function stepsForPrompt(optionsJson: string, kind: string, question: string, answerJson: string | null, answer: number | null, answerTextJson: string | null, intent?: PromptIntent): KeyStep[];