terminal-agent-workboard 0.0.1

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/dist/status.js ADDED
@@ -0,0 +1,230 @@
1
+ const statusOrder = [
2
+ "Needs input",
3
+ "Rate limited",
4
+ "Working",
5
+ "Return pending",
6
+ "WIP",
7
+ "human handoff",
8
+ "session switching",
9
+ "agent routing",
10
+ "Completed"
11
+ ];
12
+ export function orderedStatuses() {
13
+ return statusOrder;
14
+ }
15
+ export function inferStatus(capture, exists) {
16
+ if (!exists)
17
+ return "Completed";
18
+ const text = capture.toLowerCase();
19
+ if (isAgentRateLimited(capture)) {
20
+ return "Rate limited";
21
+ }
22
+ if (isAgentInitializationBlocked(capture)) {
23
+ return "Needs input";
24
+ }
25
+ if (extractCodexPromptText(capture)) {
26
+ return "Needs input";
27
+ }
28
+ if (hasIdleAgentPrompt(capture)) {
29
+ return "Needs input";
30
+ }
31
+ if (/(need(s)? input|waiting for input|approve|confirm|permission|continue\?|y\/n|\[y\/n\])/.test(text)) {
32
+ return "Needs input";
33
+ }
34
+ if (/(handoff|human handoff|manual intervention|blocked by human)/.test(text)) {
35
+ return "human handoff";
36
+ }
37
+ if (/(routing|route to|select agent|agent routing)/.test(text)) {
38
+ return "agent routing";
39
+ }
40
+ if (/(switching|switched session|session switching|attach-session)/.test(text)) {
41
+ return "session switching";
42
+ }
43
+ if (/(return pending|to return|pending return)/.test(text)) {
44
+ return "Return pending";
45
+ }
46
+ if (hasCompletedLine(capture)) {
47
+ return "Completed";
48
+ }
49
+ if (/(wip|draft|in progress)/.test(text)) {
50
+ return "WIP";
51
+ }
52
+ return "Working";
53
+ }
54
+ export function summarize(capture) {
55
+ if (isAgentRateLimited(capture)) {
56
+ return rateLimitedMessage();
57
+ }
58
+ if (isAgentInitializationBlocked(capture)) {
59
+ return initializationBlockedMessage();
60
+ }
61
+ const codexPromptText = extractCodexPromptText(capture);
62
+ if (codexPromptText) {
63
+ return `Codex prompt awaiting input: ${codexPromptText}`.slice(0, 96);
64
+ }
65
+ const latestMessageLines = extractLatestMessageLines(capture);
66
+ if (latestMessageLines.length > 0) {
67
+ return latestMessageLines[0]?.replace(/\s+/g, " ").slice(0, 96) ?? "session started";
68
+ }
69
+ const lines = capture
70
+ .split(/\r?\n/)
71
+ .map((line) => line.trim())
72
+ .filter(Boolean)
73
+ .map(cleanLine)
74
+ .filter(Boolean)
75
+ .filter((line) => !isPromptLine(line))
76
+ .filter((line) => !isFooterLine(line))
77
+ .filter((line) => !isCodexNoiseLine(line));
78
+ const assistantLine = [...lines].reverse().find((line) => isAssistantLine(line));
79
+ const last = assistantLine ?? lines.at(-1) ?? "session started";
80
+ return stripAssistantPrefix(last).replace(/\s+/g, " ").slice(0, 96);
81
+ }
82
+ export function extractLatestMessageLines(capture) {
83
+ if (isAgentRateLimited(capture)) {
84
+ return [rateLimitedMessage()];
85
+ }
86
+ if (isAgentInitializationBlocked(capture)) {
87
+ return [initializationBlockedMessage()];
88
+ }
89
+ const rawLines = capture.split(/\r?\n/);
90
+ const assistantStart = findLastIndex(rawLines, (line) => isAssistantLine(cleanLine(line.trim())));
91
+ if (assistantStart < 0)
92
+ return [];
93
+ const lines = [];
94
+ for (const rawLine of rawLines.slice(assistantStart)) {
95
+ const line = cleanLine(rawLine.trimEnd());
96
+ if (!line) {
97
+ if (lines.length > 0)
98
+ lines.push("");
99
+ continue;
100
+ }
101
+ if (lines.length > 0 && (isPromptLine(line) || isFooterLine(line) || isThinkingLine(line))) {
102
+ break;
103
+ }
104
+ if (lines.length > 0 && /^Claude Code v/i.test(line)) {
105
+ break;
106
+ }
107
+ if (isAssistantLine(line)) {
108
+ lines.push(stripAssistantPrefix(line));
109
+ continue;
110
+ }
111
+ lines.push(line);
112
+ }
113
+ return trimBlankEdges(lines).slice(0, 40);
114
+ }
115
+ export function ageLabel(iso) {
116
+ const elapsed = Math.max(0, Date.now() - new Date(iso).getTime());
117
+ const seconds = Math.floor(elapsed / 1000);
118
+ if (seconds < 60)
119
+ return `${Math.max(1, seconds)}s`;
120
+ const minutes = Math.floor(seconds / 60);
121
+ if (minutes < 60)
122
+ return `${minutes}m`;
123
+ const hours = Math.floor(minutes / 60);
124
+ if (hours < 24)
125
+ return `${hours}h`;
126
+ return `${Math.floor(hours / 24)}d`;
127
+ }
128
+ export function isAgentInitializationBlocked(capture) {
129
+ const text = capture.toLowerCase();
130
+ return (/update available/.test(text) &&
131
+ /press enter to continue/.test(text) &&
132
+ /(update now|skip until next version|release notes)/.test(text));
133
+ }
134
+ export function initializationBlockedMessage() {
135
+ return "Agent initialization needs input. Use /open or Ctrl+O to finish setup in tmux.";
136
+ }
137
+ export function isAgentRateLimited(capture) {
138
+ const text = capture.toLowerCase();
139
+ return (/approaching rate limits?/.test(text) ||
140
+ /rate limit (?:reached|exceeded|hit)/.test(text) ||
141
+ /(?:daily|weekly|monthly|usage|quota|5-?h(?:our)?) limit (?:reached|exceeded|hit)/.test(text) ||
142
+ /you'?ve reached your [^\n]{0,60}(?:usage|quota|rate) limit/.test(text) ||
143
+ /(?:quota|usage) exhausted/.test(text) ||
144
+ /usage limit hit/.test(text) ||
145
+ /out of (?:credits|quota|tokens)/.test(text));
146
+ }
147
+ export function rateLimitedMessage() {
148
+ return "Agent quota/rate limit reached. Use /open or Ctrl+O to switch model or wait for reset.";
149
+ }
150
+ function hasIdleAgentPrompt(capture) {
151
+ const lines = capture
152
+ .split(/\r?\n/)
153
+ .map((line) => cleanLine(line.trim()))
154
+ .filter(Boolean)
155
+ .filter((line) => !isFooterLine(line))
156
+ .filter((line) => !isCodexNoiseLine(line));
157
+ const lastMeaningful = lines.at(-1) ?? "";
158
+ const hasAssistantResponse = lines.some((line) => isAssistantLine(line));
159
+ return hasAssistantResponse && isPromptLine(lastMeaningful);
160
+ }
161
+ function cleanLine(line) {
162
+ return line
163
+ .replace(/\u00a0/g, " ")
164
+ .replace(/[│╭╮╰╯─]+/g, "")
165
+ .trim();
166
+ }
167
+ function isPromptLine(line) {
168
+ return /^[>❯›$#]\s*$/.test(line) || /^[\w.-]+[$#]\s*$/.test(line);
169
+ }
170
+ function isFooterLine(line) {
171
+ return (/^\? for shortcuts\b/.test(line) ||
172
+ /^← for agents\b/.test(line) ||
173
+ /^for shortcuts ·/.test(line) ||
174
+ /for shortcuts · .*for agents/.test(line));
175
+ }
176
+ function isAssistantLine(line) {
177
+ return /^[⏺●]\s+/.test(line);
178
+ }
179
+ function isThinkingLine(line) {
180
+ return /^[✻✽✶]\s+/.test(line);
181
+ }
182
+ function hasCompletedLine(capture) {
183
+ return capture
184
+ .split(/\r?\n/)
185
+ .map((line) => cleanLine(line.trim()).toLowerCase())
186
+ .some((line) => line === "done" ||
187
+ line === "completed" ||
188
+ /^done[.!:]?$/.test(line) ||
189
+ /^completed[.!:]?$/.test(line) ||
190
+ /\ball tests pass\b/.test(line) ||
191
+ /\bpr #[0-9]+ merged\b/.test(line) ||
192
+ /^success\b/.test(line) ||
193
+ /^finished\b/.test(line));
194
+ }
195
+ function extractCodexPromptText(capture) {
196
+ const promptLine = capture
197
+ .split(/\r?\n/)
198
+ .map((line) => cleanLine(line.trim()))
199
+ .reverse()
200
+ .find((line) => /^›\s+\S/.test(line));
201
+ if (!promptLine)
202
+ return null;
203
+ return promptLine.replace(/^›\s+/, "").trim() || null;
204
+ }
205
+ function isCodexNoiseLine(line) {
206
+ return (/^gpt-[\w.-]+\s+.*·/.test(line) ||
207
+ /^model:\s+/.test(line) ||
208
+ /^directory:\s+/.test(line) ||
209
+ /^tip:\s+/i.test(line) ||
210
+ /^learn more:/i.test(line));
211
+ }
212
+ function stripAssistantPrefix(line) {
213
+ return line.replace(/^[⏺●]\s+/, "");
214
+ }
215
+ function trimBlankEdges(lines) {
216
+ let start = 0;
217
+ let end = lines.length;
218
+ while (start < end && !lines[start]?.trim())
219
+ start++;
220
+ while (end > start && !lines[end - 1]?.trim())
221
+ end--;
222
+ return lines.slice(start, end);
223
+ }
224
+ function findLastIndex(items, predicate) {
225
+ for (let index = items.length - 1; index >= 0; index--) {
226
+ if (predicate(items[index]))
227
+ return index;
228
+ }
229
+ return -1;
230
+ }
package/dist/store.js ADDED
@@ -0,0 +1,32 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ export class SessionStore {
4
+ path;
5
+ constructor(cwd) {
6
+ this.path = join(cwd, ".agent-workboard", "state.json");
7
+ }
8
+ read() {
9
+ if (!existsSync(this.path))
10
+ return [];
11
+ const parsed = JSON.parse(readFileSync(this.path, "utf8"));
12
+ return parsed.sessions ?? [];
13
+ }
14
+ write(sessions) {
15
+ mkdirSync(dirname(this.path), { recursive: true });
16
+ writeFileSync(this.path, `${JSON.stringify({ sessions }, null, 2)}\n`);
17
+ }
18
+ upsert(session) {
19
+ const sessions = this.read();
20
+ const index = sessions.findIndex((candidate) => candidate.id === session.id);
21
+ if (index >= 0) {
22
+ sessions[index] = session;
23
+ }
24
+ else {
25
+ sessions.push(session);
26
+ }
27
+ this.write(sessions);
28
+ }
29
+ remove(id) {
30
+ this.write(this.read().filter((session) => session.id !== id));
31
+ }
32
+ }
@@ -0,0 +1,86 @@
1
+ import { useEffect, useRef } from "react";
2
+ import { useStdin } from "ink";
3
+ const CTRL_LEFT = new Set(["\u001B[1;5D", "\u001B[5D"]);
4
+ const CTRL_RIGHT = new Set(["\u001B[1;5C", "\u001B[5C"]);
5
+ const META_LEFT = new Set(["\u001B[1;3D", "\u001B[3D", "\u001Bb"]);
6
+ const META_RIGHT = new Set(["\u001B[1;3C", "\u001B[3C", "\u001Bf"]);
7
+ const HOME = new Set(["\u001B[H", "\u001B[1~", "\u001B[7~", "\u001BOH"]);
8
+ const END = new Set(["\u001B[F", "\u001B[4~", "\u001B[8~", "\u001BOF"]);
9
+ const SHIFT_RETURN = new Set([
10
+ "\n",
11
+ "\u001B\r",
12
+ "\u001B\n",
13
+ "\u001B[13;2u",
14
+ "\u001B[13;2~",
15
+ "\u001B[27;2;13~"
16
+ ]);
17
+ export function parseTerminalInput(data) {
18
+ const raw = String(data);
19
+ let input = raw;
20
+ const key = {
21
+ upArrow: raw === "\u001B[A",
22
+ downArrow: raw === "\u001B[B",
23
+ leftArrow: raw === "\u001B[D" || CTRL_LEFT.has(raw) || META_LEFT.has(raw),
24
+ rightArrow: raw === "\u001B[C" || CTRL_RIGHT.has(raw) || META_RIGHT.has(raw),
25
+ home: HOME.has(raw),
26
+ end: END.has(raw),
27
+ return: raw === "\r" || SHIFT_RETURN.has(raw),
28
+ escape: raw === "\u001B",
29
+ ctrl: CTRL_LEFT.has(raw) || CTRL_RIGHT.has(raw),
30
+ shift: SHIFT_RETURN.has(raw),
31
+ tab: raw === "\t" || raw === "\u001B[Z",
32
+ backspace: raw === "\u007F" || raw === "\b",
33
+ delete: raw === "\u001B[3~" || raw === "\u001B[P",
34
+ meta: META_LEFT.has(raw) || META_RIGHT.has(raw)
35
+ };
36
+ if (raw.length === 1 && raw <= "\u001A" && !key.return) {
37
+ input = String.fromCharCode(raw.charCodeAt(0) + "a".charCodeAt(0) - 1);
38
+ key.ctrl = true;
39
+ }
40
+ if (raw.startsWith("\u001B") && !isKnownEscape(key)) {
41
+ input = raw.slice(1);
42
+ key.meta = true;
43
+ }
44
+ if (key.tab && raw === "\u001B[Z")
45
+ key.shift = true;
46
+ if (key.tab || key.backspace || key.delete)
47
+ input = "";
48
+ return { input, key };
49
+ }
50
+ export function useTerminalInput(handler, options = {}) {
51
+ const { stdin, setRawMode } = useStdin();
52
+ const isActive = options.isActive ?? true;
53
+ const handlerRef = useRef(handler);
54
+ handlerRef.current = handler;
55
+ useEffect(() => {
56
+ if (!isActive)
57
+ return;
58
+ setRawMode(true);
59
+ return () => setRawMode(false);
60
+ }, [isActive, setRawMode]);
61
+ useEffect(() => {
62
+ if (!isActive)
63
+ return;
64
+ const onData = (data) => {
65
+ const parsed = parseTerminalInput(data);
66
+ handlerRef.current(parsed.input, parsed.key);
67
+ };
68
+ stdin?.on("data", onData);
69
+ return () => {
70
+ stdin?.off("data", onData);
71
+ };
72
+ }, [isActive, stdin]);
73
+ }
74
+ function isKnownEscape(key) {
75
+ return (key.upArrow ||
76
+ key.downArrow ||
77
+ key.leftArrow ||
78
+ key.rightArrow ||
79
+ key.home ||
80
+ key.end ||
81
+ key.return ||
82
+ key.tab ||
83
+ key.delete ||
84
+ key.ctrl ||
85
+ key.meta);
86
+ }
package/dist/tmux.js ADDED
@@ -0,0 +1,42 @@
1
+ import { execFileSync, spawnSync } from "node:child_process";
2
+ export class Tmux {
3
+ available() {
4
+ const result = spawnSync("tmux", ["-V"], { stdio: "ignore" });
5
+ return result.status === 0;
6
+ }
7
+ hasSession(name) {
8
+ const result = spawnSync("tmux", ["has-session", "-t", name], { stdio: "ignore" });
9
+ return result.status === 0;
10
+ }
11
+ newSession(name, cwd, command) {
12
+ execFileSync("tmux", ["new-session", "-d", "-s", name, "-c", cwd, command], {
13
+ stdio: "ignore"
14
+ });
15
+ }
16
+ sendKeys(name, input) {
17
+ execFileSync("tmux", ["send-keys", "-t", name, "-l", input], {
18
+ stdio: "ignore"
19
+ });
20
+ execFileSync("tmux", ["send-keys", "-t", name, "C-m"], {
21
+ stdio: "ignore"
22
+ });
23
+ }
24
+ capture(name, lines = 80) {
25
+ try {
26
+ return execFileSync("tmux", ["capture-pane", "-pt", name, "-S", `-${lines}`], {
27
+ encoding: "utf8",
28
+ stdio: ["ignore", "pipe", "ignore"]
29
+ });
30
+ }
31
+ catch {
32
+ return "";
33
+ }
34
+ }
35
+ kill(name) {
36
+ spawnSync("tmux", ["kill-session", "-t", name], { stdio: "ignore" });
37
+ }
38
+ attach(name) {
39
+ process.stdout.write("\x1b[2J\x1b[H");
40
+ spawnSync("tmux", ["attach-session", "-t", name], { stdio: "inherit" });
41
+ }
42
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};