@capagents/forgeqe 0.0.0-stage → 1.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 (220) hide show
  1. package/HOW_TO_USE.md +1639 -0
  2. package/README.md +338 -2
  3. package/bin/forgeqe.d.ts +3 -0
  4. package/bin/forgeqe.js +188 -0
  5. package/docs/ARCHITECTURE.md +408 -0
  6. package/docs/security-data-flow.html +1870 -0
  7. package/docs/user-guide.html +2094 -0
  8. package/evals/domain/access-role-change.yaml +37 -0
  9. package/evals/domain/banking-payment-transfer.yaml +41 -0
  10. package/evals/domain/gdpr-account-erasure.yaml +30 -0
  11. package/evals/domain/insurance-claim-fnol.yaml +30 -0
  12. package/evals/domain/insurance-policy-renewal.yaml +37 -0
  13. package/evals/domain/lending-loan-application.yaml +36 -0
  14. package/forgeqe-architecture.html +718 -0
  15. package/forgeqe.example.yaml +203 -0
  16. package/knowledge/README.md +18 -0
  17. package/knowledge/business-rules.md +10 -0
  18. package/knowledge/compliance.md +16 -0
  19. package/knowledge/environments.md +5 -0
  20. package/knowledge/glossary.md +5 -0
  21. package/knowledge/product.md +16 -0
  22. package/knowledge/roles.md +9 -0
  23. package/knowledge/test-data.md +7 -0
  24. package/llms.example.json +51 -0
  25. package/package.json +63 -4
  26. package/prompts/README.md +34 -0
  27. package/prompts/api-scenarios.md +21 -0
  28. package/prompts/api-tests.md +39 -0
  29. package/prompts/brain.md +166 -0
  30. package/prompts/browser-capture.md +7 -0
  31. package/prompts/browser-case-report.md +7 -0
  32. package/prompts/browser-checks.md +14 -0
  33. package/prompts/browser-efficiency.md +7 -0
  34. package/prompts/browser-error-recovery.md +15 -0
  35. package/prompts/browser-extra.md +5 -0
  36. package/prompts/browser-goal-verification.md +13 -0
  37. package/prompts/browser-skill-select.md +6 -0
  38. package/prompts/domain-rule.md +13 -0
  39. package/prompts/eval-judge.md +19 -0
  40. package/prompts/explore-charters.md +27 -0
  41. package/prompts/find-tests.md +28 -0
  42. package/prompts/heal.md +40 -0
  43. package/prompts/pr-review.md +29 -0
  44. package/prompts/skill-rule.md +6 -0
  45. package/prompts/test-agent.md +34 -0
  46. package/prompts/test-reviewer.md +30 -0
  47. package/prompts/test-rules.md +28 -0
  48. package/py/forgeqe_engine/__init__.py +3 -0
  49. package/py/forgeqe_engine/__main__.py +3 -0
  50. package/py/forgeqe_engine/brand.py +453 -0
  51. package/py/forgeqe_engine/brand_assets.py +6 -0
  52. package/py/forgeqe_engine/browser.py +196 -0
  53. package/py/forgeqe_engine/capture.py +538 -0
  54. package/py/forgeqe_engine/checks.py +98 -0
  55. package/py/forgeqe_engine/cli.py +188 -0
  56. package/py/forgeqe_engine/diagnostics.py +609 -0
  57. package/py/forgeqe_engine/llm.py +195 -0
  58. package/py/forgeqe_engine/logins.py +251 -0
  59. package/py/forgeqe_engine/loops.py +224 -0
  60. package/py/forgeqe_engine/memory.py +1004 -0
  61. package/py/forgeqe_engine/network.py +244 -0
  62. package/py/forgeqe_engine/prompts.py +20 -0
  63. package/py/forgeqe_engine/replay.py +240 -0
  64. package/py/forgeqe_engine/report.py +173 -0
  65. package/py/forgeqe_engine/service.py +1335 -0
  66. package/py/forgeqe_engine/settings.py +93 -0
  67. package/py/forgeqe_engine/skills.py +88 -0
  68. package/py/forgeqe_engine/summary.py +239 -0
  69. package/py/forgeqe_engine/urls.py +81 -0
  70. package/py/forgeqe_engine/workflow_yaml.py +689 -0
  71. package/skills/dotnet-nunit-playwright/SKILL.md +34 -0
  72. package/skills/dynamics-365/SKILL.md +21 -0
  73. package/skills/playwright-typescript/SKILL.md +35 -0
  74. package/skills/writing-skills/SKILL.md +30 -0
  75. package/src/api/plan.ts +246 -0
  76. package/src/api/run.ts +204 -0
  77. package/src/api/scenario.ts +502 -0
  78. package/src/api/schema.ts +170 -0
  79. package/src/api/spec.ts +624 -0
  80. package/src/api/store.ts +232 -0
  81. package/src/api/traffic.ts +281 -0
  82. package/src/api/types.ts +163 -0
  83. package/src/api/write.ts +272 -0
  84. package/src/auth/signin.ts +494 -0
  85. package/src/auth/store.ts +170 -0
  86. package/src/brain/index.ts +189 -0
  87. package/src/brain/prompt.ts +19 -0
  88. package/src/capabilities/docs.ts +101 -0
  89. package/src/capabilities/guide.ts +455 -0
  90. package/src/capabilities/registry.ts +343 -0
  91. package/src/capabilities/render.ts +106 -0
  92. package/src/capabilities/status.ts +137 -0
  93. package/src/card.ts +11 -0
  94. package/src/cases/ado.ts +325 -0
  95. package/src/cases/api.ts +117 -0
  96. package/src/cases/bugs.ts +223 -0
  97. package/src/cases/confluence.ts +111 -0
  98. package/src/cases/context.ts +223 -0
  99. package/src/cases/dashboards.ts +137 -0
  100. package/src/cases/failure.ts +97 -0
  101. package/src/cases/files.ts +295 -0
  102. package/src/cases/goal.ts +53 -0
  103. package/src/cases/http.ts +111 -0
  104. package/src/cases/index.ts +131 -0
  105. package/src/cases/jira.ts +122 -0
  106. package/src/cases/qtest.ts +108 -0
  107. package/src/cases/quality.ts +190 -0
  108. package/src/cases/rag.ts +100 -0
  109. package/src/cases/readiness.ts +284 -0
  110. package/src/cases/save.ts +149 -0
  111. package/src/cases/sprints.ts +148 -0
  112. package/src/cases/testrail.ts +176 -0
  113. package/src/cases/types.ts +107 -0
  114. package/src/cases/wiki.ts +105 -0
  115. package/src/cases/xray.ts +206 -0
  116. package/src/cases/zephyr.ts +153 -0
  117. package/src/childEnv.ts +86 -0
  118. package/src/cli/args.ts +114 -0
  119. package/src/cli/help.ts +167 -0
  120. package/src/cli/index.ts +1142 -0
  121. package/src/cli/terminal.ts +187 -0
  122. package/src/code/areas.ts +337 -0
  123. package/src/code/attention.ts +214 -0
  124. package/src/code/find.ts +540 -0
  125. package/src/code/handoff.ts +541 -0
  126. package/src/code/history.ts +170 -0
  127. package/src/code/impact.ts +231 -0
  128. package/src/code/index.ts +34 -0
  129. package/src/code/livebrowser.ts +67 -0
  130. package/src/code/loop.ts +603 -0
  131. package/src/code/notes.ts +75 -0
  132. package/src/code/profile.ts +226 -0
  133. package/src/code/prompt.ts +303 -0
  134. package/src/code/prreview.ts +649 -0
  135. package/src/code/prtests.ts +103 -0
  136. package/src/code/replay.ts +772 -0
  137. package/src/code/review.ts +202 -0
  138. package/src/code/runtime.ts +757 -0
  139. package/src/code/slices.ts +159 -0
  140. package/src/code/testrun.ts +187 -0
  141. package/src/code/traceability.ts +100 -0
  142. package/src/code/worktree.ts +153 -0
  143. package/src/commands.ts +101 -0
  144. package/src/config.ts +970 -0
  145. package/src/detail.ts +65 -0
  146. package/src/domain.ts +19 -0
  147. package/src/engine/bridge.ts +359 -0
  148. package/src/engine/mock.ts +102 -0
  149. package/src/engine/setup.ts +298 -0
  150. package/src/entra.ts +154 -0
  151. package/src/env.ts +24 -0
  152. package/src/evals/domain.ts +338 -0
  153. package/src/explore/charters.ts +151 -0
  154. package/src/explore/run.ts +280 -0
  155. package/src/explore/story.ts +106 -0
  156. package/src/guard.ts +51 -0
  157. package/src/heal/loop.ts +357 -0
  158. package/src/heal/prompt.ts +143 -0
  159. package/src/heal/report.ts +95 -0
  160. package/src/index.ts +12 -0
  161. package/src/init.ts +102 -0
  162. package/src/knowledge.ts +114 -0
  163. package/src/legacyEnv.ts +9 -0
  164. package/src/llm.ts +61 -0
  165. package/src/locale.ts +25 -0
  166. package/src/logins.ts +148 -0
  167. package/src/managed.ts +101 -0
  168. package/src/mcp/apitools.ts +335 -0
  169. package/src/mcp/captools.ts +25 -0
  170. package/src/mcp/casetools.ts +223 -0
  171. package/src/mcp/commands.ts +421 -0
  172. package/src/mcp/exploretools.ts +129 -0
  173. package/src/mcp/external.ts +172 -0
  174. package/src/mcp/git.ts +78 -0
  175. package/src/mcp/notifytools.ts +49 -0
  176. package/src/mcp/prtools.ts +261 -0
  177. package/src/mcp/qualitytools.ts +154 -0
  178. package/src/mcp/repotests.ts +286 -0
  179. package/src/mcp/server.ts +234 -0
  180. package/src/mcp/tools.ts +810 -0
  181. package/src/memory.ts +250 -0
  182. package/src/money.ts +39 -0
  183. package/src/monitor/alerts.ts +94 -0
  184. package/src/monitor/background.ts +68 -0
  185. package/src/monitor/run.ts +263 -0
  186. package/src/monitor/state.ts +105 -0
  187. package/src/notify/events.ts +144 -0
  188. package/src/notify/notice.ts +177 -0
  189. package/src/prices.ts +175 -0
  190. package/src/print.ts +54 -0
  191. package/src/privacy.ts +35 -0
  192. package/src/prompts.ts +165 -0
  193. package/src/quality/budgets.ts +249 -0
  194. package/src/quality/history.ts +149 -0
  195. package/src/quality/visual.ts +201 -0
  196. package/src/report/data.ts +475 -0
  197. package/src/report/html.ts +106 -0
  198. package/src/report/template.html +84 -0
  199. package/src/run.ts +243 -0
  200. package/src/sessionLog.ts +216 -0
  201. package/src/sessionLogins.ts +94 -0
  202. package/src/skills/registry.ts +217 -0
  203. package/src/skills/scaffold.ts +65 -0
  204. package/src/skills/select.ts +37 -0
  205. package/src/skills/view.ts +44 -0
  206. package/src/suite/report.ts +135 -0
  207. package/src/suite/run.ts +303 -0
  208. package/src/summary.ts +188 -0
  209. package/src/tools.ts +130 -0
  210. package/src/tui/App.tsx +983 -0
  211. package/src/tui/clipboard.ts +36 -0
  212. package/src/tui/components.tsx +263 -0
  213. package/src/tui/dialogs.ts +65 -0
  214. package/src/tui/main.tsx +20 -0
  215. package/src/tui/text.ts +28 -0
  216. package/src/tui/theme.ts +104 -0
  217. package/src/tui/transcript.tsx +344 -0
  218. package/src/types.ts +136 -0
  219. package/src/update.ts +311 -0
  220. package/src/usage.ts +158 -0
@@ -0,0 +1,36 @@
1
+ import { spawnSync } from "node:child_process"
2
+
3
+ /** The system clipboard command for this machine, tried in order; the first one that runs wins. */
4
+ function nativeCommands(): string[][] {
5
+ if (process.platform === "darwin") return [["pbcopy"]]
6
+ if (process.platform === "win32") return [["clip"]]
7
+ const wayland = process.env.WAYLAND_DISPLAY ? [["wl-copy"]] : []
8
+ return [...wayland, ["xclip", "-selection", "clipboard"], ["xsel", "--clipboard", "--input"]]
9
+ }
10
+
11
+ /**
12
+ * Put text on the clipboard two ways: OSC 52 through the terminal (works over SSH and in VS Code, iTerm2, kitty,
13
+ * WezTerm, Windows Terminal) and the system command (works in terminals without OSC 52, such as macOS Terminal).
14
+ */
15
+ export function copyText(text: string, osc52?: (text: string) => boolean): boolean {
16
+ let copied = false
17
+ try {
18
+ copied = osc52?.(text) === true
19
+ } catch {}
20
+ for (const [cmd, ...args] of nativeCommands()) {
21
+ try {
22
+ const r = spawnSync(cmd, args, { input: text, stdio: ["pipe", "ignore", "ignore"], timeout: 2000 })
23
+ if (r.status === 0) return true
24
+ } catch {}
25
+ }
26
+ return copied
27
+ }
28
+
29
+ /** Selected transcript text as the user wants it pasted: the card bars and the padding the layout adds removed. */
30
+ export function cleanSelection(text: string): string {
31
+ return text
32
+ .split("\n")
33
+ .map((line) => line.replace(/^\s*[┃╹]\s?/, "").trimEnd())
34
+ .join("\n")
35
+ .replace(/^\n+|\n+$/g, "")
36
+ }
@@ -0,0 +1,263 @@
1
+ import type { KeyBinding, TextareaRenderable } from "@opentui/core"
2
+ import { useEffect, useRef, useState, type ReactNode } from "react"
3
+
4
+ import { logoRows, theme } from "./theme.ts"
5
+
6
+ export const BAR = {
7
+ topLeft: "┃",
8
+ topRight: " ",
9
+ bottomLeft: "┃",
10
+ bottomRight: " ",
11
+ horizontal: " ",
12
+ vertical: "┃",
13
+ topT: " ",
14
+ bottomT: " ",
15
+ leftT: "┃",
16
+ rightT: " ",
17
+ cross: " ",
18
+ }
19
+
20
+ export function Logo() {
21
+ const name = logoRows("forge")
22
+ const suffix = logoRows("QE")
23
+ return (
24
+ <box style={{ flexDirection: "column", alignItems: "flex-end" }}>
25
+ {name.map((row, i) => (
26
+ <text key={i}>
27
+ <span fg={theme.faint}>{row}</span>
28
+ <span fg={theme.text}>{` ${suffix[i]}`}</span>
29
+ </text>
30
+ ))}
31
+ <text fg={theme.muted}>by Capgemini</text>
32
+ </box>
33
+ )
34
+ }
35
+
36
+ export type PromptProps = {
37
+ width: number
38
+ inputKey: number
39
+ onSubmit: (value: string) => void
40
+ /** Text rows the box shows when empty; it grows with the text up to maxRows, then scrolls. */
41
+ minRows: number
42
+ maxRows: number
43
+ placeholder: string
44
+ modeColor: string
45
+ profile: string
46
+ model: string
47
+ right?: string
48
+ rightColor?: string
49
+ /** A secret being entered: the box shows one dot per character instead of a text field, so the value is never drawn. */
50
+ masked?: number
51
+ }
52
+
53
+ /** Enter sends; Shift+Enter, Alt+Enter or Ctrl+J start a new line. */
54
+ const PROMPT_KEYS: KeyBinding[] = [
55
+ { name: "return", action: "submit" },
56
+ { name: "kpenter", action: "submit" },
57
+ { name: "return", shift: true, action: "newline" },
58
+ { name: "return", meta: true, action: "newline" },
59
+ { name: "linefeed", action: "newline" },
60
+ ]
61
+
62
+ export function Prompt(props: PromptProps) {
63
+ const edge = `╹${"▀".repeat(Math.max(0, props.width - 1))}`
64
+ const ref = useRef<TextareaRenderable>(null)
65
+ const [rows, setRows] = useState(props.minRows)
66
+ useEffect(() => setRows(props.minRows), [props.inputKey, props.minRows])
67
+ const fit = () => {
68
+ const lines = ref.current?.virtualLineCount ?? 1
69
+ setRows(Math.min(props.maxRows, Math.max(props.minRows, lines)))
70
+ }
71
+ return (
72
+ <box style={{ flexDirection: "column", width: props.width, flexShrink: 0 }}>
73
+ <box
74
+ border={["left"]}
75
+ borderColor={props.modeColor}
76
+ customBorderChars={BAR}
77
+ style={{
78
+ flexDirection: "column",
79
+ backgroundColor: theme.panel,
80
+ paddingLeft: 2,
81
+ paddingRight: 2,
82
+ paddingTop: 1,
83
+ paddingBottom: 1,
84
+ width: props.width,
85
+ }}
86
+ >
87
+ {props.masked !== undefined ? (
88
+ <text style={{ height: 1 }}>
89
+ {props.masked ? <span fg={theme.text}>{"•".repeat(Math.min(props.masked, Math.max(1, props.width - 8)))}</span> : <span fg={theme.faint}>{props.placeholder}</span>}
90
+ </text>
91
+ ) : (
92
+ <textarea
93
+ key={props.inputKey}
94
+ ref={ref}
95
+ placeholder={props.placeholder}
96
+ focused
97
+ wrapMode="word"
98
+ keyBindings={PROMPT_KEYS}
99
+ onSubmit={() => props.onSubmit(ref.current?.plainText ?? "")}
100
+ onContentChange={fit}
101
+ style={{ height: rows }}
102
+ backgroundColor={theme.panel}
103
+ focusedBackgroundColor={theme.panel}
104
+ textColor={theme.text}
105
+ focusedTextColor={theme.text}
106
+ placeholderColor={theme.faint}
107
+ />
108
+ )}
109
+ <box style={{ flexDirection: "row", justifyContent: "space-between", marginTop: 1 }}>
110
+ <text>
111
+ <span fg={theme.muted}>{"Model "}</span>
112
+ <span fg={theme.text}>{props.model || "…"}</span>
113
+ </text>
114
+ <text>
115
+ {props.right ? <span fg={props.rightColor || theme.muted}>{props.right}</span> : null}
116
+ {props.right ? <span fg={theme.faint}>{" · "}</span> : null}
117
+ <span fg={theme.muted}>{"profile "}</span>
118
+ <span fg={theme.text}>{props.profile || "none"}</span>
119
+ </text>
120
+ </box>
121
+ </box>
122
+ <text>
123
+ <span fg={props.modeColor}>{edge.slice(0, 1)}</span>
124
+ <span fg={theme.panel}>{edge.slice(1)}</span>
125
+ </text>
126
+ </box>
127
+ )
128
+ }
129
+
130
+ const BRAILLE = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
131
+ const BAR_CELLS = 8
132
+ const TRAIL = [theme.accent, "#3a78c2", "#27507f"]
133
+
134
+ /** Animates on its own timer so only the spinner re-renders. `bar` is a sweeping dot bar, `dots` a braille spinner. */
135
+ export function Spinner({ kind = "bar", color = theme.warn }: { kind?: "bar" | "dots"; color?: string }) {
136
+ const [frame, setFrame] = useState(0)
137
+ useEffect(() => {
138
+ const timer = setInterval(() => setFrame((f) => f + 1), kind === "bar" ? 90 : 80)
139
+ return () => clearInterval(timer)
140
+ }, [kind])
141
+ if (kind === "dots") return <text fg={color}>{BRAILLE[frame % BRAILLE.length]}</text>
142
+ const period = BAR_CELLS * 2 - 2
143
+ const p = frame % period
144
+ const head = p < BAR_CELLS ? p : period - p
145
+ const behind = p < BAR_CELLS ? -1 : 1
146
+ return (
147
+ <text>
148
+ {Array.from({ length: BAR_CELLS }, (_, i) => {
149
+ const age = (i - head) * behind
150
+ const lit = age >= 0 && age < TRAIL.length
151
+ return (
152
+ <span key={i} fg={lit ? TRAIL[age] : theme.faint}>
153
+ {lit ? "■" : "·"}
154
+ </span>
155
+ )
156
+ })}
157
+ </text>
158
+ )
159
+ }
160
+
161
+ export type Hint = { key: string; label: string; onPress?: () => void }
162
+
163
+ export function Hints({ width, items, left }: { width: number; items: Hint[]; left?: ReactNode }) {
164
+ return (
165
+ <box style={{ width, height: 1, flexShrink: 0, flexDirection: "row", justifyContent: "space-between" }}>
166
+ <box style={{ flexDirection: "row" }}>{left}</box>
167
+ <box style={{ flexDirection: "row" }}>
168
+ {items.map((item, i) => (
169
+ <text key={`${item.key}-${item.label}`} onMouseDown={item.onPress}>
170
+ <span fg={theme.text}>{`${i ? " " : ""}${item.key}`}</span>
171
+ <span fg={item.onPress ? theme.muted : theme.faint}>{item.label ? ` ${item.label}` : ""}</span>
172
+ </text>
173
+ ))}
174
+ </box>
175
+ </box>
176
+ )
177
+ }
178
+
179
+ export type Choice = { id: string; label: string }
180
+
181
+ export type ChoicePanelProps = {
182
+ width: number
183
+ title: string
184
+ detail: string
185
+ color: string
186
+ options: Choice[]
187
+ selected: number
188
+ onChoose: (id: string) => void
189
+ onHover: (index: number) => void
190
+ /** One option per line, for many or long options. */
191
+ vertical?: boolean
192
+ }
193
+
194
+ /** Question with buttons, as the agent runtime shows for permissions: arrows or tab move, enter confirms, click works too. */
195
+ export function ChoicePanel(props: ChoicePanelProps) {
196
+ const edge = `╹${"▀".repeat(Math.max(0, props.width - 1))}`
197
+ return (
198
+ <box style={{ flexDirection: "column", width: props.width, flexShrink: 0 }}>
199
+ <box
200
+ border={["left"]}
201
+ borderColor={props.color}
202
+ customBorderChars={BAR}
203
+ style={{
204
+ flexDirection: "column",
205
+ backgroundColor: theme.panel,
206
+ paddingLeft: 2,
207
+ paddingRight: 2,
208
+ paddingTop: 1,
209
+ paddingBottom: 1,
210
+ width: props.width,
211
+ }}
212
+ >
213
+ <text>
214
+ <span fg={props.color}>{"△ "}</span>
215
+ <span fg={theme.text}>{props.title}</span>
216
+ </text>
217
+ {props.detail ? <text fg={theme.muted}>{` ${props.detail}`}</text> : null}
218
+ <box style={{ flexDirection: props.vertical ? "column" : "row", justifyContent: "space-between", marginTop: 1 }}>
219
+ <box style={{ flexDirection: props.vertical ? "column" : "row", alignItems: "flex-start" }}>
220
+ {props.options.map((option, i) => {
221
+ const active = i === props.selected
222
+ return (
223
+ <box
224
+ key={option.id}
225
+ onMouseDown={() => props.onChoose(option.id)}
226
+ onMouseOver={() => props.onHover(i)}
227
+ style={{ paddingLeft: 1, paddingRight: 1, marginRight: 1, backgroundColor: active ? props.color : theme.button }}
228
+ >
229
+ <text fg={active ? theme.onButton : theme.muted}>{option.label}</text>
230
+ </box>
231
+ )
232
+ })}
233
+ </box>
234
+ <text style={{ marginTop: props.vertical ? 1 : 0 }}>
235
+ <span fg={theme.text}>{props.vertical ? "↑↓" : "⇆"}</span>
236
+ <span fg={theme.muted}>{" select "}</span>
237
+ <span fg={theme.text}>{"enter"}</span>
238
+ <span fg={theme.muted}>{" confirm "}</span>
239
+ <span fg={theme.text}>{"esc"}</span>
240
+ <span fg={theme.muted}>{" cancel"}</span>
241
+ </text>
242
+ </box>
243
+ </box>
244
+ <text>
245
+ <span fg={props.color}>{edge.slice(0, 1)}</span>
246
+ <span fg={theme.panel}>{edge.slice(1)}</span>
247
+ </text>
248
+ </box>
249
+ )
250
+ }
251
+
252
+ export function Footer({ left, right, width }: { left: string; right: string; width: number }) {
253
+ const room = Math.max(8, width - right.length - 8)
254
+ const shown = left.length <= room ? left : `…${left.slice(left.length - room + 1)}`
255
+ return (
256
+ <box
257
+ style={{ flexDirection: "row", justifyContent: "space-between", paddingLeft: 2, paddingRight: 2, height: 1, flexShrink: 0 }}
258
+ >
259
+ <text fg={theme.muted}>{shown}</text>
260
+ <text fg={theme.muted}>{right}</text>
261
+ </box>
262
+ )
263
+ }
@@ -0,0 +1,65 @@
1
+ import type { AskQuestion } from "../mcp/tools.ts"
2
+ import type { Choice } from "./components.tsx"
3
+ import { theme } from "./theme.ts"
4
+
5
+ /** The choice dialogs: stop, quit, wait for the user, and the agent's questions. */
6
+
7
+ export type Dialog = "stop" | "quit" | "wait" | "ask"
8
+
9
+ export function dialogSpec(
10
+ dialog: Dialog,
11
+ running: boolean,
12
+ outDir: string,
13
+ agent = false,
14
+ waitText = "",
15
+ ask?: AskQuestion,
16
+ ): { title: string; detail: string; color: string; options: Choice[] } {
17
+ if (dialog === "ask" && ask) {
18
+ return { title: ask.title, detail: ask.detail || "", color: theme.accent, options: ask.options.map((o) => ({ id: `ask:${o.id}`, label: o.label })) }
19
+ }
20
+ if (dialog === "wait") {
21
+ return {
22
+ title: "Your turn",
23
+ detail: waitText,
24
+ color: theme.accent,
25
+ options: [
26
+ { id: "wait-done", label: "Done, continue" },
27
+ { id: "wait-cancel", label: "Cancel" },
28
+ ],
29
+ }
30
+ }
31
+ if (dialog === "stop") {
32
+ return {
33
+ title: agent ? "Stop ForgeQE's agent?" : "Stop the running agent?",
34
+ detail: agent
35
+ ? "Any browser run stops after its current action and keeps its steps. Files the agent already changed stay."
36
+ : "The browser stops after the current action. Steps recorded so far are still saved.",
37
+ color: theme.warn,
38
+ options: [
39
+ { id: "stop", label: "Stop run" },
40
+ { id: "stop-close", label: "Stop and close ForgeQE" },
41
+ { id: "cancel", label: "Keep running" },
42
+ ],
43
+ }
44
+ }
45
+ if (running) {
46
+ return {
47
+ title: "Close ForgeQE?",
48
+ detail: "A run is in progress. It stops first and its files are saved.",
49
+ color: theme.err,
50
+ options: [
51
+ { id: "stop-close", label: "Stop and close" },
52
+ { id: "cancel", label: "Cancel" },
53
+ ],
54
+ }
55
+ }
56
+ return {
57
+ title: "Close ForgeQE?",
58
+ detail: `Runs are saved in ${outDir}/. Press ctrl+c again to close straight away.`,
59
+ color: theme.err,
60
+ options: [
61
+ { id: "close", label: "Close" },
62
+ { id: "cancel", label: "Cancel" },
63
+ ],
64
+ }
65
+ }
@@ -0,0 +1,20 @@
1
+ import { createCliRenderer } from "@opentui/core"
2
+ import { createRoot } from "@opentui/react"
3
+
4
+ import { App, type TuiOptions } from "./App.tsx"
5
+
6
+ export async function startTui(options: TuiOptions): Promise<void> {
7
+ // Ctrl-C opens the close prompt; a second Ctrl-C quits.
8
+ const renderer = await createCliRenderer({ exitOnCtrlC: false })
9
+ await new Promise<void>((resolve, reject) => {
10
+ renderer.on("destroy", () => resolve())
11
+ try {
12
+ createRoot(renderer).render(<App options={options} />)
13
+ } catch (err) {
14
+ renderer.destroy()
15
+ reject(err)
16
+ }
17
+ })
18
+ }
19
+
20
+ export type { TuiOptions }
@@ -0,0 +1,28 @@
1
+ import type { AppConfig } from "../config.ts"
2
+
3
+ /** Short text for the shell: clipped lines, the model label, hosts, and the activity line from engine logs. */
4
+
5
+ export function clip(text: string, n = 72): string {
6
+ const s = text.replace(/\s+/g, " ").trim()
7
+ return s.length <= n ? s : `${s.slice(0, n - 1)}…`
8
+ }
9
+
10
+ export function modelLabel(cfg: AppConfig): string {
11
+ if (cfg.llm.provider === "none") return "none set up"
12
+ if (cfg.llm.provider === "mock") return "offline"
13
+ return `${cfg.llm.provider}/${cfg.llm.deployment || cfg.llm.model}`
14
+ }
15
+
16
+ export function hostOf(url: string): string {
17
+ try {
18
+ return new URL(url.includes("://") ? url : `https://${url}`).hostname
19
+ } catch {
20
+ return url
21
+ }
22
+ }
23
+
24
+ export function activityFromLog(text: string): string {
25
+ if (text.startsWith("$ ") || /^Setting up the ForgeQE engine/.test(text)) return "Installing the ForgeQE engine (one time)…"
26
+ if (text.startsWith("ForgeQE engine ·")) return "Starting the browser…"
27
+ return clip(text, 70)
28
+ }
@@ -0,0 +1,104 @@
1
+ import { readFileSync } from "node:fs"
2
+ import os from "node:os"
3
+ import path from "node:path"
4
+
5
+ export const theme = {
6
+ accent: "#4ea1ff",
7
+ panel: "#161b22",
8
+ text: "#e6edf3",
9
+ muted: "#8b949e",
10
+ faint: "#6e7681",
11
+ ok: "#9ece6a",
12
+ warn: "#e0af68",
13
+ err: "#f7768e",
14
+ thought: "#a5b4fc",
15
+ memory: "#c792ea",
16
+ button: "#21262d",
17
+ onButton: "#0d1117",
18
+ }
19
+
20
+ /** 950 → "950", 47_100 → "47.1K", 2_300_000 → "2.3M". */
21
+ export function formatTokens(n: number): string {
22
+ if (n < 1000) return String(n)
23
+ if (n < 1_000_000) return `${(n / 1000).toFixed(1)}K`
24
+ return `${(n / 1_000_000).toFixed(1)}M`
25
+ }
26
+
27
+ /** Whole seconds for clocks; `precise` keeps one decimal under 10s for per-step timings. */
28
+ export function formatDuration(ms: number, precise = false): string {
29
+ const s = Math.max(0, ms) / 1000
30
+ if (precise && s < 10) return `${s.toFixed(1)}s`
31
+ if (s < 60) return `${Math.floor(s)}s`
32
+ const m = Math.floor(s / 60)
33
+ return `${m}m ${String(Math.floor(s % 60)).padStart(2, "0")}s`
34
+ }
35
+
36
+ /** Four rows: ascenders, then a three-row x-height. Capitals use all four. */
37
+ const GLYPHS: Record<string, [string, string, string, string]> = {
38
+ w: [" ", "█ █", "█ █ █", "▀▀▀▀▀"],
39
+ e: [" ", "█▀▀█", "█▀▀▀", "▀▀▀▀"],
40
+ b: ["▄ ", "█▀▀█", "█ █", "▀▀▀▀"],
41
+ p: [" ", "█▀▀█", "█ █", "█▀▀▀"],
42
+ i: [" ", "▀", "█", "▀"],
43
+ l: ["▄", "█", "█", "▀"],
44
+ o: [" ", "█▀▀█", "█ █", "▀▀▀▀"],
45
+ t: [" ▄ ", "▀█▀▀", " █ ", " ▀▀▀"],
46
+ r: [" ", "█▀▀", "█ ", "▀ "],
47
+ c: [" ", "█▀▀▀", "█ ", "▀▀▀▀"],
48
+ h: ["▄ ", "█▀▀█", "█ █", "▀ ▀"],
49
+ s: [" ", "█▀▀▀", "▀▀▀█", "▀▀▀▀"],
50
+ a: [" ", "▀▀▀█", "█▀▀█", "▀▀▀▀"],
51
+ f: [" ▄▀", "▀█▀", " █ ", " ▀ "],
52
+ g: [" ", "█▀▀█", "█ █", "▀▀▀█"],
53
+ Q: ["█▀▀█ ", "█ █ ", "█ █ ", "▀▀▀█▄"],
54
+ E: ["█▀▀▀", "█▄▄ ", "█ ", "▀▀▀▀"],
55
+ ".": [" ", " ", " ", "▀"],
56
+ }
57
+
58
+ export function logoRows(word: string): string[] {
59
+ const rows = ["", "", "", ""]
60
+ const letters = [...word]
61
+ letters.forEach((ch, index) => {
62
+ const glyph = GLYPHS[ch] ?? GLYPHS[ch.toLowerCase()]
63
+ if (!glyph) return
64
+ for (let r = 0; r < 4; r++) rows[r] += (index ? " " : "") + glyph[r]
65
+ })
66
+ return rows
67
+ }
68
+
69
+ export const EXAMPLES = [
70
+ "Go to booking.com and search for hotels in Mumbai",
71
+ "Run SHOP-12 and SHOP-14 from Jira",
72
+ "Run the test cases in smoke.yaml, two browsers at once",
73
+ "Write a Playwright test for the last run",
74
+ "Add a GitHub Actions workflow that runs smoke.yaml on every PR",
75
+ ]
76
+
77
+ export const TIPS = [
78
+ "Just say what you want; ForgeQE works out whether it's a goal, test cases, a test, or a pipeline",
79
+ "Press tab to switch LLM profile",
80
+ "Use /headed on to watch the agent drive Chrome",
81
+ "A passed run becomes a test in your repo's own language and framework",
82
+ "Set allowed_hosts in forgeqe.yaml to keep runs on one domain",
83
+ "Re-running a goal replays it from page memory, mostly without the LLM",
84
+ "Ask what page memory knows about a site to see its locators and confidence",
85
+ ]
86
+
87
+ export function pick<T>(items: T[]): T {
88
+ return items[Math.floor(Math.random() * items.length)]
89
+ }
90
+
91
+ export function shortCwd(): string {
92
+ const cwd = process.cwd()
93
+ const home = os.homedir()
94
+ return cwd === home || cwd.startsWith(home + path.sep) ? `~${cwd.slice(home.length)}` : cwd
95
+ }
96
+
97
+ export function packageVersion(): string {
98
+ try {
99
+ const file = path.resolve(import.meta.dir, "..", "..", "package.json")
100
+ return String(JSON.parse(readFileSync(file, "utf8")).version || "")
101
+ } catch {
102
+ return ""
103
+ }
104
+ }