@dalmasonto/taskflow-mcp 1.0.35 → 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 (101) 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 +121 -218
  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 -28
  65. package/dist/agent-registry.js +0 -158
  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/retry.d.ts +0 -34
  73. package/dist/retry.js +0 -94
  74. package/dist/sse.d.ts +0 -10
  75. package/dist/sse.js +0 -824
  76. package/dist/tmux-bridge.d.ts +0 -13
  77. package/dist/tmux-bridge.js +0 -217
  78. package/dist/tools/activity.d.ts +0 -39
  79. package/dist/tools/activity.js +0 -152
  80. package/dist/tools/agent-inbox.d.ts +0 -12
  81. package/dist/tools/agent-inbox.js +0 -272
  82. package/dist/tools/agent.d.ts +0 -14
  83. package/dist/tools/agent.js +0 -168
  84. package/dist/tools/analytics.d.ts +0 -21
  85. package/dist/tools/analytics.js +0 -191
  86. package/dist/tools/checkpoint.d.ts +0 -27
  87. package/dist/tools/checkpoint.js +0 -105
  88. package/dist/tools/notifications.d.ts +0 -31
  89. package/dist/tools/notifications.js +0 -59
  90. package/dist/tools/projects.d.ts +0 -55
  91. package/dist/tools/projects.js +0 -112
  92. package/dist/tools/settings.d.ts +0 -19
  93. package/dist/tools/settings.js +0 -73
  94. package/dist/tools/tasks.d.ts +0 -105
  95. package/dist/tools/tasks.js +0 -403
  96. package/dist/tools/terminal.d.ts +0 -4
  97. package/dist/tools/terminal.js +0 -98
  98. package/dist/tools/timer.d.ts +0 -37
  99. package/dist/tools/timer.js +0 -154
  100. package/dist/types.d.ts +0 -83
  101. package/dist/types.js +0 -30
@@ -0,0 +1,211 @@
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
+ import { answerKeystrokes } from "./events.js";
23
+ function asOptions(value) {
24
+ if (!Array.isArray(value))
25
+ return [];
26
+ return value.filter((o) => typeof o === "object" && o !== null && typeof o.number === "number");
27
+ }
28
+ /**
29
+ * The questions a prompt is asking.
30
+ *
31
+ * `fallbackKind` and `fallbackQuestion` come from the row's own columns and are
32
+ * used only for the legacy shape, which stored the question text and kind there
33
+ * rather than inside the JSON.
34
+ */
35
+ export function parseQuestions(optionsJson, fallbackKind, fallbackQuestion) {
36
+ let parsed;
37
+ try {
38
+ parsed = JSON.parse(optionsJson);
39
+ }
40
+ catch {
41
+ return [];
42
+ }
43
+ if (!Array.isArray(parsed) || parsed.length === 0)
44
+ return [];
45
+ // The discriminator is a nested `options` array: the set shape wraps each
46
+ // question, the legacy shape IS the option list.
47
+ const isSet = parsed.every((entry) => typeof entry === "object" && entry !== null && "options" in entry);
48
+ if (!isSet) {
49
+ const options = asOptions(parsed);
50
+ return options.length ? [{ question: fallbackQuestion, kind: fallbackKind, options }] : [];
51
+ }
52
+ return parsed
53
+ .map((entry) => {
54
+ const e = entry;
55
+ return {
56
+ question: typeof e.question === "string" ? e.question : fallbackQuestion,
57
+ kind: typeof e.kind === "string" ? e.kind : "single",
58
+ options: asOptions(e.options),
59
+ };
60
+ })
61
+ .filter((q) => q.options.length > 0);
62
+ }
63
+ /**
64
+ * The chosen numbers, one set per question.
65
+ *
66
+ * `answer` is the row's single-answer column, kept as a fallback for rows
67
+ * answered before `answer_json` existed.
68
+ */
69
+ export function parseAnswerSets(answerJson, answer) {
70
+ const numeric = (value) => Array.isArray(value) ? value.filter((n) => typeof n === "number") : [];
71
+ if (answerJson) {
72
+ try {
73
+ const parsed = JSON.parse(answerJson);
74
+ if (Array.isArray(parsed)) {
75
+ // `[[1],[2]]` is one set per question; `[1,3]` is a single flat set.
76
+ return parsed.some(Array.isArray)
77
+ ? parsed.map(numeric).filter((set) => set.length > 0)
78
+ : [numeric(parsed)].filter((set) => set.length > 0);
79
+ }
80
+ }
81
+ catch {
82
+ // fall through to the single-answer column
83
+ }
84
+ }
85
+ return typeof answer === "number" ? [[answer]] : [];
86
+ }
87
+ /** Whether every question in the set has at least one choice. */
88
+ export function isFullyAnswered(sets, questionCount) {
89
+ if (questionCount === 0)
90
+ return false;
91
+ if (sets.length !== questionCount)
92
+ return false;
93
+ return sets.every((set) => set.length > 0);
94
+ }
95
+ /**
96
+ * The review screen a MULTI-QUESTION set lands on once every question has been
97
+ * answered:
98
+ *
99
+ * Ready to submit your answers?
100
+ * > 1. Submit answers
101
+ * 2. Cancel
102
+ *
103
+ * Verified against a live session on 2026-07-21. Without this stage the agent
104
+ * answered all three questions and then sat on the review screen forever — the
105
+ * questions were right, and nothing pressed the final key.
106
+ *
107
+ * A numbered choice, so it takes a number AND Enter, matching the single-select
108
+ * behaviour confirmed the same day.
109
+ */
110
+ const REVIEW_SUBMIT = ["1", "Enter"];
111
+ const REVIEW_CANCEL = ["2", "Enter"];
112
+ /**
113
+ * The full keystroke sequence for an answered prompt, or `[]` when it must not
114
+ * be replayed.
115
+ *
116
+ * Each question contributes its own sequence and they run back to back, in the
117
+ * order the questions were asked — the terminal advances to the next question as
118
+ * each is submitted. A set then confirms at the review screen.
119
+ *
120
+ * A single SINGLE-select question has no review screen (it submits as soon as it
121
+ * is answered), so nothing is appended and there is no safe "cancel" key. A
122
+ * single MULTI-select question DOES reach the review screen, so it gets the
123
+ * submit/cancel like a set does.
124
+ */
125
+ export function keystrokesForPrompt(optionsJson, kind, question, answerJson, answer, intent = "submit") {
126
+ const questions = parseQuestions(optionsJson, kind, question);
127
+ const sets = parseAnswerSets(answerJson, answer);
128
+ if (!isFullyAnswered(sets, questions.length))
129
+ return [];
130
+ // The review screen appears for a question SET *and* for a single
131
+ // multi-select question — both end on "Ready to submit your answers?".
132
+ // Gating only on question count (as it did) left a lone multi-select stuck on
133
+ // that screen with nothing to press it. Verified live 2026-07-21.
134
+ const hasReviewScreen = questions.length > 1 || questions.some((q) => q.kind === "multi");
135
+ if (intent === "cancel" && !hasReviewScreen)
136
+ return [];
137
+ const answers = questions.flatMap((q, i) => answerKeystrokes(sets[i] ?? [], q.kind));
138
+ if (!hasReviewScreen)
139
+ return answers;
140
+ // Cancel is reached the same way as submit: every answer is replayed first,
141
+ // because the review screen only exists once the last question is answered.
142
+ return [...answers, ...(intent === "cancel" ? REVIEW_CANCEL : REVIEW_SUBMIT)];
143
+ }
144
+ /** The Other free-text value per question, parallel to the answer sets. */
145
+ export function parseAnswerTexts(answerTextJson, count) {
146
+ if (!answerTextJson)
147
+ return Array(count).fill(null);
148
+ try {
149
+ const parsed = JSON.parse(answerTextJson);
150
+ if (Array.isArray(parsed)) {
151
+ return Array.from({ length: count }, (_, i) => typeof parsed[i] === "string" ? parsed[i] : null);
152
+ }
153
+ }
154
+ catch { /* fall through */ }
155
+ return Array(count).fill(null);
156
+ }
157
+ /**
158
+ * The full ordered steps for an answered prompt. Without any Other text this is
159
+ * `keystrokesForPrompt` wrapped as `{key}` steps. With Other text on a single
160
+ * multi-select question, it walks the arrow-based flow the new TUI uses (see the
161
+ * hypothesis in the test — verified live before this ships).
162
+ */
163
+ export function stepsForPrompt(optionsJson, kind, question, answerJson, answer, answerTextJson, intent = "submit") {
164
+ const questions = parseQuestions(optionsJson, kind, question);
165
+ const sets = parseAnswerSets(answerJson, answer);
166
+ const texts = parseAnswerTexts(answerTextJson, questions.length);
167
+ // Only the scoped case gets the Other flow: one multi-select question whose
168
+ // Other option was picked with text. Everything else uses the proven path.
169
+ const single = questions.length === 1 ? questions[0] : undefined;
170
+ const other = single?.options.find((o) => o.isOther);
171
+ const otherPicked = other && sets[0]?.includes(other.number) && (texts[0] ?? "").length > 0;
172
+ if (!otherPicked || intent === "cancel") {
173
+ return keystrokesForPrompt(optionsJson, kind, question, answerJson, answer, intent)
174
+ .map((key) => ({ key }));
175
+ }
176
+ const otherIndex = single.options.findIndex((o) => o.isOther);
177
+ // SINGLE-select Other (#30): one answer, no toggles, no review screen. Arrow
178
+ // down from the caret on option 1 to the "Type something" row, type the text
179
+ // (which fills the field), and Enter submits it — single-select commits on
180
+ // Enter, unlike multi-select where Enter only toggles. HYPOTHESIS, verified
181
+ // live before shipping (the choreography is the one thing tests can't prove).
182
+ if (single.kind !== "multi") {
183
+ const singleSteps = [];
184
+ for (let i = 0; i < otherIndex; i++)
185
+ singleSteps.push({ key: "Down" });
186
+ singleSteps.push({ text: texts[0] });
187
+ singleSteps.push({ key: "Enter" });
188
+ return singleSteps;
189
+ }
190
+ const steps = [];
191
+ // Walk down the list from the caret's start on option 1, toggling picks.
192
+ for (let i = 0; i < single.options.length; i++) {
193
+ const opt = single.options[i];
194
+ if (opt.isOther) {
195
+ // Typing (which send-keys -l mimics) AUTO-CHECKS the Other option, and
196
+ // Enter merely TOGGLES it — so NO Enter here, or it would deselect the box
197
+ // the paste just checked. Verified live 2026-07-21: with an Enter, Other
198
+ // came back unchecked and dropped from the answer.
199
+ steps.push({ text: texts[0] });
200
+ }
201
+ else if (sets[0].includes(opt.number)) {
202
+ steps.push({ key: "Enter" }); // toggle this pick
203
+ }
204
+ if (i < single.options.length - 1)
205
+ steps.push({ key: "Down" });
206
+ }
207
+ steps.push({ key: "Down" }, { key: "Enter" }); // to Submit, activate -> review
208
+ steps.push({ key: "1" }, { key: "Enter" }); // Submit answers
209
+ return steps;
210
+ }
211
+ //# sourceMappingURL=prompts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompts.js","sourceRoot":"","sources":["../src/prompts.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Turn an id-only realtime message event into the message itself.
3
+ *
4
+ * ## Why this exists
5
+ *
6
+ * Chat rows are CHANNEL-scoped but the realtime group is per-PROJECT, so
7
+ * anything projected onto the wire reaches every member of the project. That is
8
+ * why chat events carry the row id and nothing else — projecting the fields
9
+ * broadcast every DM's body, roster and attachment key to the whole project
10
+ * (`backend/tests/realtime_dm_privacy.rs`).
11
+ *
12
+ * The id alone is safe to broadcast and useless without authorization: the
13
+ * message is fetched back over the agent read API, which re-checks the channel
14
+ * roster. An agent that is not on the channel resolves nothing.
15
+ *
16
+ * The event carries no channel either, so the lookup fans out across the
17
+ * channels this agent is on — the same shape `check_messages` already uses. That
18
+ * roster is small (a project room plus a handful of DMs), and the per-channel
19
+ * fetch is a one-row window rather than a page.
20
+ */
21
+ import type { MessageTarget } from "./events.js";
22
+ /** A message as the agent read API returns it. */
23
+ export interface ResolvedMessage {
24
+ id: number;
25
+ channel: number;
26
+ sender_kind: string;
27
+ sender_label: string;
28
+ body_markdown: string;
29
+ sender_agent: number | null;
30
+ /** Present on the read row (the backend serializes them); the agent notice
31
+ * surfaces them so a delivered message carries its own context. */
32
+ project?: number | null;
33
+ task?: number | null;
34
+ sender_user?: number | null;
35
+ /** #29: the one agent a directed group message is for; null = broadcast. */
36
+ target_agent?: number | null;
37
+ /** #29: the full directed-target set (agents + users). The backend stores it
38
+ * as a JSON STRING column, so the read row carries a string here; this module
39
+ * parses it into an array before delivery. Supersedes `target_agent`. */
40
+ targets?: MessageTarget[];
41
+ attachments?: Array<{
42
+ name: string;
43
+ size_bytes: number;
44
+ url: string;
45
+ }>;
46
+ }
47
+ /** Parse the message's `targets` column — a JSON string like
48
+ * `[{"kind":"agent","id":1}]` on the read row — into a validated array.
49
+ * Anything malformed (bad JSON, wrong shape) degrades to undefined, i.e. a
50
+ * broadcast, rather than throwing on the delivery path. */
51
+ export declare function parseTargets(raw: unknown): MessageTarget[] | undefined;
52
+ /** The slice of the client this module needs; narrowed so tests need no HTTP. */
53
+ export interface MessageSource {
54
+ listChannels(): Promise<Array<{
55
+ id: number;
56
+ }>>;
57
+ listMessages(params: {
58
+ channel: number;
59
+ since?: number;
60
+ limit?: number;
61
+ }): Promise<{
62
+ messages: ResolvedMessage[];
63
+ }>;
64
+ }
65
+ /**
66
+ * Fetch message `id`, or `null` when this agent cannot see it.
67
+ *
68
+ * Never throws. A failure to resolve must degrade to "nothing delivered" — the
69
+ * message is still stored and `check_messages` will surface it — rather than
70
+ * taking down the event stream that carries every other message.
71
+ */
72
+ export declare function resolveMessage(source: MessageSource, id: number): Promise<ResolvedMessage | null>;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Turn an id-only realtime message event into the message itself.
3
+ *
4
+ * ## Why this exists
5
+ *
6
+ * Chat rows are CHANNEL-scoped but the realtime group is per-PROJECT, so
7
+ * anything projected onto the wire reaches every member of the project. That is
8
+ * why chat events carry the row id and nothing else — projecting the fields
9
+ * broadcast every DM's body, roster and attachment key to the whole project
10
+ * (`backend/tests/realtime_dm_privacy.rs`).
11
+ *
12
+ * The id alone is safe to broadcast and useless without authorization: the
13
+ * message is fetched back over the agent read API, which re-checks the channel
14
+ * roster. An agent that is not on the channel resolves nothing.
15
+ *
16
+ * The event carries no channel either, so the lookup fans out across the
17
+ * channels this agent is on — the same shape `check_messages` already uses. That
18
+ * roster is small (a project room plus a handful of DMs), and the per-channel
19
+ * fetch is a one-row window rather than a page.
20
+ */
21
+ /** Parse the message's `targets` column — a JSON string like
22
+ * `[{"kind":"agent","id":1}]` on the read row — into a validated array.
23
+ * Anything malformed (bad JSON, wrong shape) degrades to undefined, i.e. a
24
+ * broadcast, rather than throwing on the delivery path. */
25
+ export function parseTargets(raw) {
26
+ let value = raw;
27
+ if (typeof raw === "string") {
28
+ if (raw.trim() === "")
29
+ return undefined;
30
+ try {
31
+ value = JSON.parse(raw);
32
+ }
33
+ catch {
34
+ return undefined;
35
+ }
36
+ }
37
+ if (!Array.isArray(value))
38
+ return undefined;
39
+ const out = [];
40
+ for (const entry of value) {
41
+ if (entry && typeof entry === "object") {
42
+ const kind = entry.kind;
43
+ const id = entry.id;
44
+ if (typeof kind === "string" && typeof id === "number")
45
+ out.push({ kind, id });
46
+ }
47
+ }
48
+ return out.length ? out : undefined;
49
+ }
50
+ /**
51
+ * Fetch message `id`, or `null` when this agent cannot see it.
52
+ *
53
+ * Never throws. A failure to resolve must degrade to "nothing delivered" — the
54
+ * message is still stored and `check_messages` will surface it — rather than
55
+ * taking down the event stream that carries every other message.
56
+ */
57
+ export async function resolveMessage(source, id) {
58
+ let channels;
59
+ try {
60
+ channels = await source.listChannels();
61
+ }
62
+ catch {
63
+ return null;
64
+ }
65
+ const found = await Promise.all(channels.map(async (channel) => {
66
+ try {
67
+ // `since` is exclusive, so `id - 1` asks for exactly this row onward and
68
+ // `limit: 1` keeps it to one. A per-event page fetch would be wasteful on
69
+ // a busy project.
70
+ const page = await source.listMessages({ channel: channel.id, since: id - 1, limit: 1 });
71
+ // The window can return a NEIGHBOURING row when this id is not in that
72
+ // channel, so match the id explicitly — delivering the wrong message
73
+ // would be worse than delivering none.
74
+ return page.messages.find((m) => m.id === id) ?? null;
75
+ }
76
+ catch {
77
+ // One unreachable channel must not hide a message sitting in another.
78
+ return null;
79
+ }
80
+ }));
81
+ const message = found.find((m) => m !== null) ?? null;
82
+ // The read row carries `targets` as a JSON string; parse it to the array
83
+ // `shouldDeliver` gates on. Done here so both the live push and the reconnect
84
+ // catch-up (which resolve through this function) see the same parsed shape.
85
+ if (message)
86
+ message.targets = parseTargets(message.targets);
87
+ return message;
88
+ }
89
+ //# sourceMappingURL=resolve.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve.js","sourceRoot":"","sources":["../src/resolve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AA0BH;;;4DAG4D;AAC5D,MAAM,UAAU,YAAY,CAAC,GAAY;IACvC,IAAI,KAAK,GAAY,GAAG,CAAC;IACzB,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;QAC5B,IAAI,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE;YAAE,OAAO,SAAS,CAAC;QACxC,IAAI,CAAC;YACH,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC1B,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,SAAS,CAAC;QACnB,CAAC;IACH,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IAC5C,MAAM,GAAG,GAAoB,EAAE,CAAC;IAChC,KAAK,MAAM,KAAK,IAAI,KAAK,EAAE,CAAC;QAC1B,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YACvC,MAAM,IAAI,GAAI,KAAiC,CAAC,IAAI,CAAC;YACrD,MAAM,EAAE,GAAI,KAAiC,CAAC,EAAE,CAAC;YACjD,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,OAAO,EAAE,KAAK,QAAQ;gBAAE,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;QACjF,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC;AACtC,CAAC;AAYD;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,MAAqB,EACrB,EAAU;IAEV,IAAI,QAA+B,CAAC;IACpC,IAAI,CAAC;QACH,QAAQ,GAAG,MAAM,MAAM,CAAC,YAAY,EAAE,CAAC;IACzC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,GAAG,CAC7B,QAAQ,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE;QAC7B,IAAI,CAAC;YACH,yEAAyE;YACzE,0EAA0E;YAC1E,kBAAkB;YAClB,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,EAAE,KAAK,EAAE,EAAE,GAAG,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;YACzF,uEAAuE;YACvE,qEAAqE;YACrE,uCAAuC;YACvC,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,IAAI,IAAI,CAAC;QACxD,CAAC;QAAC,MAAM,CAAC;YACP,sEAAsE;YACtE,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC,CAAC,CACH,CAAC;IAEF,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAwB,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC,IAAI,IAAI,CAAC;IAC5E,yEAAyE;IACzE,8EAA8E;IAC9E,4EAA4E;IAC5E,IAAI,OAAO;QAAE,OAAO,CAAC,OAAO,GAAG,YAAY,CAAE,OAAiC,CAAC,OAAO,CAAC,CAAC;IACxF,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Everything that runs ONCE THERE IS A LIVE SESSION: the agent event stream
3
+ * (message delivery, prompt replay, terminal keys) and the tmux pane mirror.
4
+ *
5
+ * Extracted from `index.ts` because two callers need it — the bin entry at
6
+ * startup, and `select_profile` once a human has chosen an identity — and
7
+ * because a 170-line closure inside a bin entry is not a unit anyone can
8
+ * reason about.
9
+ *
10
+ * The pane is optional. Without one there is nothing to mirror and nowhere to
11
+ * type a message, so only the parts that need a pane are skipped; the session
12
+ * itself is already live and `check_messages` still works.
13
+ *
14
+ * It also holds the STARTUP entry (`startAgent`) and the two ways in — the bin
15
+ * entry at boot, and `select_profile` once a human has chosen an identity — so
16
+ * both take exactly the same path to a live agent.
17
+ */
18
+ import type { ConnectedContext } from "./connect.js";
19
+ import { type ResolvedProfile } from "./config.js";
20
+ /**
21
+ * Attach the event stream (and, with a pane, the terminal mirror) to a live
22
+ * session.
23
+ *
24
+ * Returns a teardown that detaches BOTH. A profile switch must be able to call
25
+ * it: the mirror keeps calling `appendFrame(oldSession, …)` every couple of
26
+ * seconds and the backend reads a frame as proof of life, so an abandoned
27
+ * runtime keeps the OLD identity online in the dashboard however thoroughly its
28
+ * heartbeat was stopped — and its SSE stream keeps delivering that identity's
29
+ * DMs into this pane and marking them read as it. Idempotent.
30
+ */
31
+ export declare function startAgentRuntime(ctx: ConnectedContext, log: (line: string) => void): () => void;
32
+ /**
33
+ * Bring this agent online: resolve which identity this terminal is, then
34
+ * connect and start the runtime. When the repo defines several identities and
35
+ * nothing says which one this is, connect NOTHING and record `needs_profile` —
36
+ * guessing would silently collapse two terminals into one agent.
37
+ *
38
+ * Deliberately NOT conditional on tmux: registering a session is what makes the
39
+ * agent visible and reachable, and that must happen whether or not there is a
40
+ * pane to mirror.
41
+ */
42
+ export declare function startAgent(options?: {
43
+ configPath?: string;
44
+ }): Promise<void>;
45
+ /**
46
+ * Connect as a known profile and start the runtime. Used by `select_profile`.
47
+ *
48
+ * Nothing is awaited: `startConnection`'s `settled` stays pending for as long as
49
+ * the backend is down, so awaiting it here would keep the MCP server from
50
+ * serving a single tool call until the backend came up.
51
+ */
52
+ export declare function connectAs(profile: ResolvedProfile, pane: string | null): void;
53
+ /** Persist a human's pick for this terminal, then connect as it. */
54
+ export declare function selectProfile(profile: ResolvedProfile): Promise<void>;