ghost-dragon 4.2.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.
Files changed (226) hide show
  1. package/.github/workflows/ci.yml +23 -0
  2. package/CHANGELOG.md +96 -0
  3. package/README.md +193 -0
  4. package/bootstrap.ps1 +83 -0
  5. package/bootstrap.sh +71 -0
  6. package/dist/agent/loop.d.ts +68 -0
  7. package/dist/agent/loop.d.ts.map +1 -0
  8. package/dist/agent/loop.js +135 -0
  9. package/dist/agent/mcp.d.ts +33 -0
  10. package/dist/agent/mcp.d.ts.map +1 -0
  11. package/dist/agent/mcp.js +107 -0
  12. package/dist/agent/session.d.ts +16 -0
  13. package/dist/agent/session.d.ts.map +1 -0
  14. package/dist/agent/session.js +55 -0
  15. package/dist/agent/skills.d.ts +36 -0
  16. package/dist/agent/skills.d.ts.map +1 -0
  17. package/dist/agent/skills.js +153 -0
  18. package/dist/agent/stack.d.ts +21 -0
  19. package/dist/agent/stack.d.ts.map +1 -0
  20. package/dist/agent/stack.js +158 -0
  21. package/dist/agent/task.d.ts +21 -0
  22. package/dist/agent/task.d.ts.map +1 -0
  23. package/dist/agent/task.js +45 -0
  24. package/dist/agent/tools.d.ts +44 -0
  25. package/dist/agent/tools.d.ts.map +1 -0
  26. package/dist/agent/tools.js +262 -0
  27. package/dist/agent/trace.d.ts +34 -0
  28. package/dist/agent/trace.d.ts.map +1 -0
  29. package/dist/agent/trace.js +72 -0
  30. package/dist/agent.d.ts +46 -0
  31. package/dist/agent.d.ts.map +1 -0
  32. package/dist/agent.js +103 -0
  33. package/dist/auth.d.ts +74 -0
  34. package/dist/auth.d.ts.map +1 -0
  35. package/dist/auth.js +116 -0
  36. package/dist/brain/anthropic.d.ts +19 -0
  37. package/dist/brain/anthropic.d.ts.map +1 -0
  38. package/dist/brain/anthropic.js +74 -0
  39. package/dist/brain/claude-cli.d.ts +20 -0
  40. package/dist/brain/claude-cli.d.ts.map +1 -0
  41. package/dist/brain/claude-cli.js +79 -0
  42. package/dist/brain/ghost-ember.d.ts +28 -0
  43. package/dist/brain/ghost-ember.d.ts.map +1 -0
  44. package/dist/brain/ghost-ember.js +97 -0
  45. package/dist/brain/index.d.ts +22 -0
  46. package/dist/brain/index.d.ts.map +1 -0
  47. package/dist/brain/index.js +95 -0
  48. package/dist/brain/openai-compat.d.ts +21 -0
  49. package/dist/brain/openai-compat.d.ts.map +1 -0
  50. package/dist/brain/openai-compat.js +119 -0
  51. package/dist/brain/router/classify.d.ts +23 -0
  52. package/dist/brain/router/classify.d.ts.map +1 -0
  53. package/dist/brain/router/classify.js +160 -0
  54. package/dist/brain/router/execute.d.ts +23 -0
  55. package/dist/brain/router/execute.d.ts.map +1 -0
  56. package/dist/brain/router/execute.js +84 -0
  57. package/dist/brain/router/index.d.ts +26 -0
  58. package/dist/brain/router/index.d.ts.map +1 -0
  59. package/dist/brain/router/index.js +118 -0
  60. package/dist/brain/router/routing-memory.d.ts +27 -0
  61. package/dist/brain/router/routing-memory.d.ts.map +1 -0
  62. package/dist/brain/router/routing-memory.js +77 -0
  63. package/dist/brain/router/select.d.ts +32 -0
  64. package/dist/brain/router/select.d.ts.map +1 -0
  65. package/dist/brain/router/select.js +146 -0
  66. package/dist/brain/router/two-hop.d.ts +23 -0
  67. package/dist/brain/router/two-hop.d.ts.map +1 -0
  68. package/dist/brain/router/two-hop.js +39 -0
  69. package/dist/brain/router/verify.d.ts +37 -0
  70. package/dist/brain/router/verify.d.ts.map +1 -0
  71. package/dist/brain/router/verify.js +111 -0
  72. package/dist/brain/types.d.ts +55 -0
  73. package/dist/brain/types.d.ts.map +1 -0
  74. package/dist/brain/types.js +16 -0
  75. package/dist/brain/worker.d.ts +27 -0
  76. package/dist/brain/worker.d.ts.map +1 -0
  77. package/dist/brain/worker.js +71 -0
  78. package/dist/commands/ai.d.ts +24 -0
  79. package/dist/commands/ai.d.ts.map +1 -0
  80. package/dist/commands/ai.js +137 -0
  81. package/dist/commands/alerts.d.ts +19 -0
  82. package/dist/commands/alerts.d.ts.map +1 -0
  83. package/dist/commands/alerts.js +114 -0
  84. package/dist/commands/billing.d.ts +13 -0
  85. package/dist/commands/billing.d.ts.map +1 -0
  86. package/dist/commands/billing.js +55 -0
  87. package/dist/commands/chat.d.ts +22 -0
  88. package/dist/commands/chat.d.ts.map +1 -0
  89. package/dist/commands/chat.js +422 -0
  90. package/dist/commands/config.d.ts +18 -0
  91. package/dist/commands/config.d.ts.map +1 -0
  92. package/dist/commands/config.js +136 -0
  93. package/dist/commands/doctor.d.ts +11 -0
  94. package/dist/commands/doctor.d.ts.map +1 -0
  95. package/dist/commands/doctor.js +73 -0
  96. package/dist/commands/global.d.ts +11 -0
  97. package/dist/commands/global.d.ts.map +1 -0
  98. package/dist/commands/global.js +253 -0
  99. package/dist/commands/keep.d.ts +12 -0
  100. package/dist/commands/keep.d.ts.map +1 -0
  101. package/dist/commands/keep.js +58 -0
  102. package/dist/commands/lifecycle.d.ts +17 -0
  103. package/dist/commands/lifecycle.d.ts.map +1 -0
  104. package/dist/commands/lifecycle.js +267 -0
  105. package/dist/commands/login.d.ts +16 -0
  106. package/dist/commands/login.d.ts.map +1 -0
  107. package/dist/commands/login.js +234 -0
  108. package/dist/commands/maintenance.d.ts +12 -0
  109. package/dist/commands/maintenance.d.ts.map +1 -0
  110. package/dist/commands/maintenance.js +76 -0
  111. package/dist/commands/mcp.d.ts +16 -0
  112. package/dist/commands/mcp.d.ts.map +1 -0
  113. package/dist/commands/mcp.js +56 -0
  114. package/dist/commands/memory.d.ts +13 -0
  115. package/dist/commands/memory.d.ts.map +1 -0
  116. package/dist/commands/memory.js +218 -0
  117. package/dist/commands/osint.d.ts +14 -0
  118. package/dist/commands/osint.d.ts.map +1 -0
  119. package/dist/commands/osint.js +161 -0
  120. package/dist/commands/pentest.d.ts +13 -0
  121. package/dist/commands/pentest.d.ts.map +1 -0
  122. package/dist/commands/pentest.js +131 -0
  123. package/dist/commands/scale.d.ts +14 -0
  124. package/dist/commands/scale.d.ts.map +1 -0
  125. package/dist/commands/scale.js +191 -0
  126. package/dist/commands/serve.d.ts +16 -0
  127. package/dist/commands/serve.d.ts.map +1 -0
  128. package/dist/commands/serve.js +167 -0
  129. package/dist/commands/tui.d.ts +17 -0
  130. package/dist/commands/tui.d.ts.map +1 -0
  131. package/dist/commands/tui.js +138 -0
  132. package/dist/commands/wyrm.d.ts +20 -0
  133. package/dist/commands/wyrm.d.ts.map +1 -0
  134. package/dist/commands/wyrm.js +274 -0
  135. package/dist/config.d.ts +67 -0
  136. package/dist/config.d.ts.map +1 -0
  137. package/dist/config.js +54 -0
  138. package/dist/index.d.ts +16 -0
  139. package/dist/index.d.ts.map +1 -0
  140. package/dist/index.js +85 -0
  141. package/dist/manifest.d.ts +31 -0
  142. package/dist/manifest.d.ts.map +1 -0
  143. package/dist/manifest.js +83 -0
  144. package/dist/ui.d.ts +57 -0
  145. package/dist/ui.d.ts.map +1 -0
  146. package/dist/ui.js +174 -0
  147. package/dist/utils.d.ts +33 -0
  148. package/dist/utils.d.ts.map +1 -0
  149. package/dist/utils.js +155 -0
  150. package/dist/wyrm/mcp.d.ts +37 -0
  151. package/dist/wyrm/mcp.d.ts.map +1 -0
  152. package/dist/wyrm/mcp.js +137 -0
  153. package/docs/SYSTEM-PREMORTEM.md +397 -0
  154. package/dragon-manifest.toml +241 -0
  155. package/dragon.py +177 -0
  156. package/install/launchd/lk.ghosts.dragonkeep.plist +57 -0
  157. package/install/systemd/dragonkeep.service +40 -0
  158. package/media/dragon-silver-lockup.svg +931 -0
  159. package/media/dragon-silver-mark.svg +931 -0
  160. package/media/dragon-silver.png +0 -0
  161. package/package.json +45 -0
  162. package/specs/001-godmode/constitution.md +54 -0
  163. package/specs/001-godmode/plan.md +30 -0
  164. package/specs/001-godmode/spec.md +64 -0
  165. package/specs/001-godmode/tasks.md +35 -0
  166. package/specs/002-premortem-positioning/premortem.md +211 -0
  167. package/src/agent/loop.ts +165 -0
  168. package/src/agent/mcp.ts +92 -0
  169. package/src/agent/session.ts +48 -0
  170. package/src/agent/skills.ts +138 -0
  171. package/src/agent/stack.ts +154 -0
  172. package/src/agent/task.ts +55 -0
  173. package/src/agent/tools.ts +255 -0
  174. package/src/agent/trace.ts +76 -0
  175. package/src/agent.ts +114 -0
  176. package/src/auth.ts +133 -0
  177. package/src/brain/anthropic.ts +83 -0
  178. package/src/brain/claude-cli.ts +78 -0
  179. package/src/brain/ghost-ember.ts +94 -0
  180. package/src/brain/index.ts +99 -0
  181. package/src/brain/openai-compat.ts +115 -0
  182. package/src/brain/router/classify.ts +167 -0
  183. package/src/brain/router/execute.ts +80 -0
  184. package/src/brain/router/index.ts +125 -0
  185. package/src/brain/router/routing-memory.ts +71 -0
  186. package/src/brain/router/select.ts +156 -0
  187. package/src/brain/router/two-hop.ts +62 -0
  188. package/src/brain/router/verify.ts +123 -0
  189. package/src/brain/types.ts +61 -0
  190. package/src/brain/worker.ts +72 -0
  191. package/src/commands/ai.ts +144 -0
  192. package/src/commands/alerts.ts +131 -0
  193. package/src/commands/billing.ts +59 -0
  194. package/src/commands/chat.ts +318 -0
  195. package/src/commands/config.ts +137 -0
  196. package/src/commands/doctor.ts +71 -0
  197. package/src/commands/global.ts +256 -0
  198. package/src/commands/keep.ts +67 -0
  199. package/src/commands/lifecycle.ts +273 -0
  200. package/src/commands/login.ts +184 -0
  201. package/src/commands/maintenance.ts +54 -0
  202. package/src/commands/mcp.ts +57 -0
  203. package/src/commands/memory.ts +229 -0
  204. package/src/commands/osint.ts +171 -0
  205. package/src/commands/pentest.ts +140 -0
  206. package/src/commands/scale.ts +185 -0
  207. package/src/commands/serve.ts +171 -0
  208. package/src/commands/tui.ts +126 -0
  209. package/src/commands/wyrm.ts +269 -0
  210. package/src/config.ts +93 -0
  211. package/src/index.ts +92 -0
  212. package/src/manifest.ts +104 -0
  213. package/src/ui.ts +188 -0
  214. package/src/utils.ts +153 -0
  215. package/src/wyrm/mcp.ts +130 -0
  216. package/test/auth.test.ts +70 -0
  217. package/test/brain.test.ts +39 -0
  218. package/test/security.test.ts +104 -0
  219. package/test/skills.test.ts +38 -0
  220. package/test/ui.test.ts +46 -0
  221. package/tsconfig.json +19 -0
  222. package/worker/package-lock.json +1527 -0
  223. package/worker/package.json +17 -0
  224. package/worker/src/index.ts +76 -0
  225. package/worker/tsconfig.json +15 -0
  226. package/worker/wrangler.toml +26 -0
package/src/ui.ts ADDED
@@ -0,0 +1,188 @@
1
+ /**
2
+ * UI design system — the "ops console" premium look (stealth silver).
3
+ *
4
+ * One place for the chrome: framed panels (boxen, ANSI-width-aware), a chrome
5
+ * gradient wordmark, status dots, key/value rows, rules + section headers. Every
6
+ * command composes these so the whole CLI reads as one flagship tool.
7
+ *
8
+ * Copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
9
+ */
10
+
11
+ import chalk from 'chalk'
12
+ import boxen from 'boxen'
13
+ import { homedir } from 'node:os'
14
+ import { C } from './utils.js'
15
+
16
+ const BORDER = '#454e57' // dim slate — the frame
17
+ const SILVER_STOPS = ['#dfe6ec', '#99a3ac', '#eef3f7', '#99a3ac', '#dfe6ec']
18
+
19
+ function hexLerp(a: string, b: string, t: number): string {
20
+ const A = [1, 3, 5].map((i) => parseInt(a.slice(i, i + 2), 16))
21
+ const B = [1, 3, 5].map((i) => parseInt(b.slice(i, i + 2), 16))
22
+ const c = A.map((v, i) => Math.round(v + (B[i] - v) * t))
23
+ return '#' + c.map((v) => v.toString(16).padStart(2, '0')).join('')
24
+ }
25
+
26
+ /** Brushed-chrome gradient across a string — the premium wordmark treatment. */
27
+ export function chrome(s: string): string {
28
+ const chars = [...s]
29
+ const n = chars.length
30
+ if (n === 0) return s
31
+ return chars
32
+ .map((ch, i) => {
33
+ if (ch === ' ') return ch
34
+ const p = (n === 1 ? 0 : i / (n - 1)) * (SILVER_STOPS.length - 1)
35
+ const lo = Math.floor(p)
36
+ const hi = Math.min(lo + 1, SILVER_STOPS.length - 1)
37
+ return chalk.hex(hexLerp(SILVER_STOPS[lo], SILVER_STOPS[hi], p - lo)).bold(ch)
38
+ })
39
+ .join('')
40
+ }
41
+
42
+ /** A framed panel with an optional left-aligned title (rendered in the border). */
43
+ export function panel(lines: string[], opts: { title?: string; borderColor?: string; width?: number } = {}): string {
44
+ return boxen(lines.join('\n'), {
45
+ title: opts.title,
46
+ titleAlignment: 'left',
47
+ padding: { top: 0, bottom: 0, left: 1, right: 2 },
48
+ borderStyle: 'round',
49
+ borderColor: opts.borderColor ?? BORDER,
50
+ ...(opts.width ? { width: opts.width } : {}),
51
+ })
52
+ }
53
+
54
+ // ghosts.lk "live/online" accent — a whisper of emerald (oklch 0.55 0.15 155).
55
+ export const emerald = chalk.hex('#2bb673')
56
+ export const emeraldDim = chalk.hex('#1c6e49')
57
+
58
+ /** Zip two rectangular blocks side-by-side. `leftWidth` = the left block's visible
59
+ * width (set a fixed `width` on the left panel so padding lines up). */
60
+ export function joinColumns(left: string, right: string, leftWidth: number, gap = 3): string {
61
+ const L = left.split('\n')
62
+ const R = right.split('\n')
63
+ const n = Math.max(L.length, R.length)
64
+ const blank = ' '.repeat(leftWidth)
65
+ const sep = ' '.repeat(gap)
66
+ const out: string[] = []
67
+ for (let i = 0; i < n; i++) out.push((L[i] ?? blank) + sep + (R[i] ?? ''))
68
+ return out.join('\n')
69
+ }
70
+
71
+ /** A brushed-chrome horizontal rule. */
72
+ export function chromeRule(width: number): string {
73
+ return chrome('─'.repeat(Math.max(4, width)))
74
+ }
75
+
76
+ // ── sub-cell data viz (the obscure terminal-graphics craft) ──
77
+ const SPARK = '▁▂▃▄▅▆▇█'
78
+ const PARTIAL = [' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉', '█'] // eighth-cell fills
79
+
80
+ /** A unicode sparkline (8 vertical levels). Emerald where there's signal, faint for zero. */
81
+ export function sparkline(values: number[]): string {
82
+ if (!values.length) return ''
83
+ const max = Math.max(...values, 1)
84
+ return values.map((v) => (v <= 0 ? C.faint('▁') : emerald(SPARK[Math.min(7, Math.max(0, Math.round((v / max) * 7)))]))).join('')
85
+ }
86
+
87
+ /** A fractional gauge bar (eighth-cell precision). Colour ramps green→amber→red. */
88
+ export function gauge(frac: number, width: number): string {
89
+ const f = Math.max(0, Math.min(1, frac))
90
+ const cells = f * width
91
+ const full = Math.floor(cells)
92
+ const rem = Math.round((cells - full) * 8)
93
+ const bar = '█'.repeat(full) + (rem > 0 ? PARTIAL[rem] : '')
94
+ const used = full + (rem > 0 ? 1 : 0)
95
+ const col = f < 0.7 ? emerald : f < 0.9 ? C.high : C.critical
96
+ return col(bar) + C.faint('·'.repeat(Math.max(0, width - used)))
97
+ }
98
+
99
+ // Braille (U+2800 + 8-bit mask) = a 2x4 dot grid per cell → 8 sub-pixels, the
100
+ // densest text-only renderer (see the terminal-subcell-graphics skill).
101
+ const BR_DOTS = [
102
+ [0x01, 0x08],
103
+ [0x02, 0x10],
104
+ [0x04, 0x20],
105
+ [0x40, 0x80],
106
+ ]
107
+
108
+ /**
109
+ * A multi-row Braille area/line chart — 4× the vertical resolution of a
110
+ * sparkline and 2× horizontal (values are interpolated across dot columns).
111
+ * Returns `height` colored rows of `width` cells. Emerald signal by default.
112
+ */
113
+ export function brailleChart(
114
+ values: number[],
115
+ opts: { width?: number; height?: number; max?: number; color?: (s: string) => string; area?: boolean } = {},
116
+ ): string[] {
117
+ const width = Math.max(2, opts.width ?? 32)
118
+ const height = Math.max(1, opts.height ?? 3)
119
+ const W = width * 2
120
+ const H = height * 4
121
+ const max = opts.max ?? Math.max(...values, 1)
122
+ const color = opts.color ?? emerald
123
+ const area = opts.area ?? true
124
+ const bits = new Uint8Array(width * height)
125
+ const set = (x: number, y: number) => {
126
+ if (x < 0 || x >= W || y < 0 || y >= H) return
127
+ bits[(y >> 2) * width + (x >> 1)] |= BR_DOTS[y & 3][x & 1]
128
+ }
129
+ const n = values.length
130
+ for (let x = 0; x < W; x++) {
131
+ const idx = n <= 1 ? 0 : (x / (W - 1)) * (n - 1)
132
+ const lo = Math.floor(idx)
133
+ const hi = Math.min(lo + 1, n - 1)
134
+ const v = (values[lo] ?? 0) + ((values[hi] ?? 0) - (values[lo] ?? 0)) * (idx - lo)
135
+ const lvl = Math.max(0, Math.min(H - 1, Math.round((v / max) * (H - 1))))
136
+ const top = H - 1 - lvl
137
+ if (area) for (let y = H - 1; y >= top; y--) set(x, y)
138
+ else set(x, top)
139
+ }
140
+ const rows: string[] = []
141
+ for (let cy = 0; cy < height; cy++) {
142
+ let s = ''
143
+ for (let cx = 0; cx < width; cx++) {
144
+ const b = bits[cy * width + cx]
145
+ s += b ? String.fromCharCode(0x2800 + b) : ' '
146
+ }
147
+ rows.push(color(s))
148
+ }
149
+ return rows
150
+ }
151
+
152
+ export const dot = {
153
+ on: C.accent('●'),
154
+ off: C.faint('○'),
155
+ live: chalk.hex('#46e0b0')('●'),
156
+ warn: C.high('●'),
157
+ crit: C.critical('●'),
158
+ }
159
+ export const statusDot = (on: boolean) => (on ? dot.on : dot.off)
160
+
161
+ /** "label value" with the label dim + padded to a column. */
162
+ export function kv(label: string, value: string, pad = 7): string {
163
+ return `${C.faint(label.padEnd(pad))} ${value}`
164
+ }
165
+
166
+ /** A hairline rule, optionally with a section label on the left. */
167
+ export function rule(width = 50, label?: string): string {
168
+ if (!label) return C.faint('─'.repeat(width))
169
+ const tag = ` ${label.toUpperCase()} `
170
+ return C.faint('──') + C.info(tag) + C.faint('─'.repeat(Math.max(0, width - tag.length - 2)))
171
+ }
172
+
173
+ /** Collapse $HOME → ~ for tidy paths. */
174
+ export function tidyPath(p: string): string {
175
+ const h = homedir()
176
+ return p.startsWith(h) ? '~' + p.slice(h.length) : p
177
+ }
178
+
179
+ /** The ops-console brand splash (global header). */
180
+ export function brandHeader(version: string): string {
181
+ return panel(
182
+ [
183
+ `${C.accent('▟█▙')} ${chrome('DRAGON')} ${C.faint('· stack control')}`,
184
+ `${C.accent('▜█▛')} ${C.faint('account.ghosts.lk · v' + version)}`,
185
+ ],
186
+ { title: chrome('GHOST PROTOCOL') },
187
+ )
188
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Shared utilities for Dragon CLI.
3
+ *
4
+ * Output palette intentionally narrow to match the Dragon Console
5
+ * web UI (one teal/green accent · severity by intensity, not hue).
6
+ */
7
+
8
+ import { execSync, spawn, type SpawnOptions } from 'child_process'
9
+ import { existsSync, readFileSync, statSync } from 'node:fs'
10
+ import { join } from 'node:path'
11
+ import { createConnection } from 'node:net'
12
+ import chalk from 'chalk'
13
+
14
+ export function exec(cmd: string, cwd?: string): string {
15
+ try {
16
+ return execSync(cmd, { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim()
17
+ } catch (e: any) {
18
+ throw new Error(e.stderr?.trim() || e.message)
19
+ }
20
+ }
21
+
22
+ export function run(cmd: string, args: string[], cwd: string, opts?: SpawnOptions): Promise<number> {
23
+ return new Promise((resolve, reject) => {
24
+ const child = spawn(cmd, args, {
25
+ cwd,
26
+ stdio: 'inherit',
27
+ ...opts,
28
+ })
29
+ child.on('close', code => resolve(code ?? 1))
30
+ child.on('error', reject)
31
+ })
32
+ }
33
+
34
+ export async function fetchJSON<T = any>(url: string, timeoutMs = 5000): Promise<T> {
35
+ const controller = new AbortController()
36
+ const timer = setTimeout(() => controller.abort(), timeoutMs)
37
+ try {
38
+ const res = await fetch(url, { signal: controller.signal })
39
+ if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`)
40
+ return res.json() as Promise<T>
41
+ } finally {
42
+ clearTimeout(timer)
43
+ }
44
+ }
45
+
46
+ // ─── brand palette: STEALTH SILVER ─────────────────────────────
47
+ // Mirrors account.ghosts.lk's monochrome scheme (near-black canvas,
48
+ // silver/white accent, zinc muted) — the operator-stealth look. The teal
49
+ // Dragon-Console accent was retired here. Severity stays in hue (function
50
+ // over stealth — a CRITICAL must read as red). Truecolor assumed; legacy
51
+ // 16-color terms fall back to the nearest chalk basic colour.
52
+
53
+ export const C = {
54
+ accent: chalk.hex('#c8d0d8'), // stealth silver · primary
55
+ hot: chalk.hex('#eef3f7'), // bright silver/white · live / running
56
+ critical: chalk.hex('#ff5a55'), // red · severity
57
+ high: chalk.hex('#fb923c'), // orange · severity
58
+ medium: chalk.hex('#fbbf24'), // amber · severity
59
+ low: chalk.hex('#8ab4e8'), // soft blue
60
+ info: chalk.hex('#9aa4ad'), // zinc grey
61
+ faint: chalk.hex('#5b6570'), // dim slate
62
+ }
63
+
64
+ // ─── output discipline ─────────────────────────────────────────
65
+
66
+ export function label(text: string): string {
67
+ return C.accent.bold(`[ ${text.toUpperCase()} ]`)
68
+ }
69
+
70
+ export function eyebrow(num: string | number, text: string): string {
71
+ return C.accent.bold(`[ ${String(num).padStart(2, '0')} // ${text.toUpperCase()} ]`)
72
+ }
73
+
74
+ export function dim(text: string): string {
75
+ return chalk.dim(text)
76
+ }
77
+
78
+ // Severity glyphs + hues match the web UI sev pills:
79
+ // CRITICAL ⚠ red · HIGH ! orange · MEDIUM amber · LOW blue · INFO grey
80
+ // CLI helpers use the matching colour so error/warn/info read at a glance.
81
+ export function error(msg: string): void { console.error(C.critical.bold(' ⚠'), msg) }
82
+ export function warn(msg: string): void { console.warn(C.high(' !'), msg) }
83
+ export function success(msg: string): void { console.log(C.accent(' ✓'), msg) }
84
+ export function info(msg: string): void { console.log(C.info(' ·'), msg) }
85
+
86
+ export function table(rows: Record<string, string>[]): void {
87
+ if (rows.length === 0) return
88
+ const keys = Object.keys(rows[0])
89
+ const widths = keys.map(k => Math.max(k.length, ...rows.map(r => (r[k] || '').length)))
90
+
91
+ const header = keys.map((k, i) => k.padEnd(widths[i])).join(' ')
92
+ const sep = widths.map(w => '─'.repeat(w)).join('──')
93
+
94
+ console.log(chalk.dim(header))
95
+ console.log(chalk.dim(sep))
96
+ rows.forEach(row => {
97
+ console.log(keys.map((k, i) => (row[k] || '').padEnd(widths[i])).join(' '))
98
+ })
99
+ }
100
+
101
+ // ─── module state probes ──────────────────────────────────────
102
+ // Used by `dragon list` + `dragon doctor` so output reflects reality,
103
+ // not "the repo dir exists therefore ✓".
104
+
105
+ export function isInstalled(dir: string): boolean {
106
+ return existsSync(join(dir, '.git'))
107
+ }
108
+
109
+ export function isRunning(port?: number, host = '127.0.0.1', timeoutMs = 200): Promise<boolean> {
110
+ return new Promise((resolve) => {
111
+ if (!port) return resolve(false)
112
+ const sock = createConnection({ host, port })
113
+ const done = (ok: boolean) => { try { sock.destroy() } catch {} resolve(ok) }
114
+ sock.once('connect', () => done(true))
115
+ sock.once('error', () => done(false))
116
+ setTimeout(() => done(false), timeoutMs)
117
+ })
118
+ }
119
+
120
+ export function readVersion(dir: string): string | null {
121
+ const parsers: Record<string, (raw: string) => string | null> = {
122
+ 'package.json': (raw) => { try { return JSON.parse(raw).version ?? null } catch { return null } },
123
+ 'Cargo.toml': (raw) => raw.match(/^version\s*=\s*"([^"]+)"/m)?.[1] ?? null,
124
+ 'pyproject.toml': (raw) => raw.match(/^version\s*=\s*"([^"]+)"/m)?.[1] ?? null,
125
+ }
126
+ // Search the repo root + a handful of conventional monorepo layouts so
127
+ // packages like Wyrm (no root manifest, version in packages/mcp-server)
128
+ // still report a version on `dragon list`.
129
+ const candidates = [
130
+ dir,
131
+ join(dir, 'packages', 'mcp-server'),
132
+ join(dir, 'packages', 'core'),
133
+ join(dir, 'packages', 'cli'),
134
+ join(dir, 'apps', 'web'),
135
+ join(dir, 'apps', 'server'),
136
+ ]
137
+ for (const c of candidates) {
138
+ for (const [fname, parse] of Object.entries(parsers)) {
139
+ const f = join(c, fname)
140
+ if (!existsSync(f)) continue
141
+ try {
142
+ const v = parse(readFileSync(f, 'utf-8'))
143
+ if (v) return v
144
+ } catch { /* keep trying */ }
145
+ }
146
+ }
147
+ return null
148
+ }
149
+
150
+ export function lastUpdated(dir: string): string | null {
151
+ try { return new Date(statSync(dir).mtime).toISOString().slice(0, 10) }
152
+ catch { return null }
153
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Deep Wyrm integration — the agent's memory spine, on by default.
3
+ *
4
+ * Spawns the Wyrm MCP server (`wyrm-mcp`) over stdio and:
5
+ * - exposes a CURATED subset of Wyrm's tools to the brain (recall, remember,
6
+ * capture, search, project/global context, quests, skills, entities, truths)
7
+ * so the model can read + write long-term memory mid-task;
8
+ * - PRIMES each session with the current project's context, folded into the
9
+ * system prompt, so Dragon starts already knowing the project.
10
+ *
11
+ * Everything is best-effort: if Wyrm isn't reachable the agent still runs (just
12
+ * without memory). Tool routing in the loop sends any `wyrm_*` call here.
13
+ *
14
+ * Copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
15
+ */
16
+
17
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js'
18
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
19
+ import { existsSync } from 'node:fs'
20
+ import { homedir } from 'node:os'
21
+ import { join } from 'node:path'
22
+ import type { ToolSpec } from '../brain/types.js'
23
+
24
+ /** High-value Wyrm tools we surface to the model (the full surface is ~116). */
25
+ const EXPOSED = new Set([
26
+ 'wyrm_recall', 'wyrm_remember', 'wyrm_capture', 'wyrm_search',
27
+ 'wyrm_project_context', 'wyrm_global_context', 'wyrm_context_build',
28
+ 'wyrm_all_quests', 'wyrm_quest_add', 'wyrm_quest_complete',
29
+ 'wyrm_skill_search', 'wyrm_skill_get',
30
+ 'wyrm_entity_search', 'wyrm_truth_get', 'wyrm_truth_set', 'wyrm_decided_because',
31
+ ])
32
+
33
+ function wyrmBin(): string {
34
+ const candidates = [process.env.WYRM_MCP_BIN, join(homedir(), '.npm-global/bin/wyrm-mcp')].filter(Boolean) as string[]
35
+ for (const c of candidates) if (existsSync(c)) return c
36
+ return 'wyrm-mcp' // fall back to PATH
37
+ }
38
+
39
+ const CONNECT_TIMEOUT = 8_000 // a slow/hanging Wyrm must never freeze the agent
40
+ const CALL_TIMEOUT = 25_000
41
+
42
+ /** Resolve to `null` if the promise doesn't settle in `ms` (running `onTimeout`). */
43
+ function withTimeout<T>(p: Promise<T>, ms: number, onTimeout?: () => void): Promise<T | null> {
44
+ return new Promise((resolve) => {
45
+ let done = false
46
+ const timer = setTimeout(() => { if (!done) { done = true; onTimeout?.(); resolve(null) } }, ms)
47
+ p.then(
48
+ (v) => { if (!done) { done = true; clearTimeout(timer); resolve(v) } },
49
+ () => { if (!done) { done = true; clearTimeout(timer); resolve(null) } },
50
+ )
51
+ })
52
+ }
53
+
54
+ export interface WyrmTool { spec: ToolSpec }
55
+
56
+ export class Wyrm {
57
+ private client: Client | null = null
58
+ private transport: StdioClientTransport | null = null
59
+ private toolNames = new Set<string>()
60
+ connected = false
61
+
62
+ /** Connect + discover tools (bounded by CONNECT_TIMEOUT). Never throws / hangs. */
63
+ async connect(): Promise<boolean> {
64
+ try {
65
+ this.transport = new StdioClientTransport({ command: wyrmBin(), args: [], stderr: 'ignore' })
66
+ this.client = new Client({ name: 'dragon-cli', version: '3.7.0' }, { capabilities: {} })
67
+ const ok = await withTimeout(
68
+ (async () => {
69
+ await this.client!.connect(this.transport!)
70
+ const { tools } = await this.client!.listTools()
71
+ this.exposed = tools
72
+ .filter((t) => EXPOSED.has(t.name))
73
+ .map((t) => ({ name: t.name, description: t.description ?? '', parameters: (t.inputSchema as Record<string, unknown>) ?? { type: 'object', properties: {} } }))
74
+ this.exposed.forEach((t) => this.toolNames.add(t.name))
75
+ return true
76
+ })(),
77
+ CONNECT_TIMEOUT,
78
+ () => { try { void this.transport?.close() } catch { /* ignore */ } },
79
+ )
80
+ this.connected = ok === true
81
+ if (!this.connected) { try { await this.client?.close() } catch { /* ignore */ } this.client = null }
82
+ return this.connected
83
+ } catch {
84
+ this.connected = false
85
+ return false
86
+ }
87
+ }
88
+
89
+ private exposed: ToolSpec[] = []
90
+
91
+ /** Curated Wyrm tool specs to hand the brain (empty if not connected). */
92
+ toolSpecs(): ToolSpec[] {
93
+ return this.exposed
94
+ }
95
+
96
+ handles(name: string): boolean {
97
+ return this.toolNames.has(name)
98
+ }
99
+
100
+ /** Invoke a Wyrm tool; returns a string result for the model (bounded). */
101
+ async call(name: string, args: Record<string, unknown>): Promise<string> {
102
+ if (!this.client) return 'error: Wyrm not connected'
103
+ const res = await withTimeout(
104
+ this.client.callTool({ name, arguments: args }) as Promise<{ content?: { type: string; text?: string }[]; isError?: boolean }>,
105
+ CALL_TIMEOUT,
106
+ )
107
+ if (res === null) return `error: ${name} timed out`
108
+ const text = (res.content ?? []).map((c) => (c.type === 'text' ? c.text ?? '' : `[${c.type}]`)).join('\n').trim()
109
+ return (res.isError ? 'error: ' : '') + (text || '(no result)')
110
+ }
111
+
112
+ /** Best-effort project context for the system prompt. Never throws. */
113
+ async prime(cwd: string): Promise<string | null> {
114
+ if (!this.client) return null
115
+ for (const [name, args] of [
116
+ ['wyrm_project_context', { projectPath: cwd }],
117
+ ['wyrm_project_context', { project: cwd }],
118
+ ] as const) {
119
+ if (!this.toolNames.has(name)) continue
120
+ const out = await this.call(name, args as Record<string, unknown>)
121
+ if (out && !out.startsWith('error')) return out.slice(0, 4000)
122
+ }
123
+ return null
124
+ }
125
+
126
+ async close(): Promise<void> {
127
+ try { await this.client?.close() } catch { /* ignore */ }
128
+ this.connected = false
129
+ }
130
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Auth resolution precedence — the "does login actually work on a fresh machine"
3
+ * guarantee, made deterministic. `resolveAuthFrom` is pure (env + config in,
4
+ * headers + mode out), so the whole token > session > none ladder, env > config
5
+ * precedence, origin self-heal, and header-injection rejection are exercised with
6
+ * zero network and zero dependence on THIS box's ~/.dragon/config.json. (Closes
7
+ * the premortem's "untested authed / single-machine path" gap.)
8
+ *
9
+ * Runs against built output: `npm run build && vitest run`.
10
+ */
11
+ import { describe, it, expect } from 'vitest'
12
+ import { resolveAuthFrom, DEFAULT_API } from '../dist/auth.js'
13
+
14
+ // resolveAuthFrom only reads `cfg.auth`; a minimal stub is all it needs.
15
+ const cfg = (auth?: Record<string, unknown>) => ({ auth } as any)
16
+
17
+ describe('resolveAuthFrom — credential precedence', () => {
18
+ it('token beats session beats none', () => {
19
+ const r = resolveAuthFrom({ DRAGON_TOKEN: 'dgn_tok123', DRAGON_SESSION: 'sess456' }, cfg())
20
+ expect(r.mode).toBe('token')
21
+ expect(r.headers.authorization).toBe('Bearer dgn_tok123')
22
+ expect(r.headers.cookie).toBeUndefined()
23
+ })
24
+ it('session when only a session is present', () => {
25
+ const r = resolveAuthFrom({ DRAGON_SESSION: 'sess456' }, cfg())
26
+ expect(r.mode).toBe('session')
27
+ expect(r.headers.cookie).toBe('gp_session=sess456')
28
+ })
29
+ it('none when nothing is present', () => {
30
+ const r = resolveAuthFrom({}, cfg())
31
+ expect(r.mode).toBe('none')
32
+ expect(r.headers).toEqual({})
33
+ })
34
+ it('env overrides config', () => {
35
+ const r = resolveAuthFrom({ DRAGON_TOKEN: 'env_tok' }, cfg({ token: 'cfg_tok' }))
36
+ expect(r.headers.authorization).toBe('Bearer env_tok')
37
+ })
38
+ it('falls back to config when env is empty, and surfaces the stored email', () => {
39
+ const r = resolveAuthFrom({}, cfg({ token: 'cfg_tok', email: 'ryan@ghosts.lk' }))
40
+ expect(r.mode).toBe('token')
41
+ expect(r.headers.authorization).toBe('Bearer cfg_tok')
42
+ expect(r.email).toBe('ryan@ghosts.lk')
43
+ })
44
+ })
45
+
46
+ describe('resolveAuthFrom — apiBase origin guard', () => {
47
+ it('defaults when unset', () => {
48
+ expect(resolveAuthFrom({}, cfg()).apiBase).toBe(DEFAULT_API)
49
+ })
50
+ it('honors a valid https override (env) and strips the trailing slash', () => {
51
+ expect(resolveAuthFrom({ DRAGON_API: 'https://staging.ghosts.lk/' }, cfg()).apiBase).toBe('https://staging.ghosts.lk')
52
+ })
53
+ it('self-heals a restricted-port / junk origin back to default', () => {
54
+ expect(resolveAuthFrom({ DRAGON_API: 'http://127.0.0.1:1' }, cfg()).apiBase).toBe(DEFAULT_API)
55
+ expect(resolveAuthFrom({ DRAGON_API: 'ftp://x' }, cfg()).apiBase).toBe(DEFAULT_API)
56
+ })
57
+ })
58
+
59
+ describe('resolveAuthFrom — header-injection rejection', () => {
60
+ it('drops a CRLF-poisoned token (no header smuggling) → session wins', () => {
61
+ const r = resolveAuthFrom({ DRAGON_TOKEN: 'tok\r\nX-Evil: 1', DRAGON_SESSION: 'cleansess' }, cfg())
62
+ expect(r.mode).toBe('session')
63
+ expect(r.headers.authorization).toBeUndefined()
64
+ expect(r.headers.cookie).toBe('gp_session=cleansess')
65
+ })
66
+ it('drops a poisoned token AND a poisoned session → none', () => {
67
+ const r = resolveAuthFrom({ DRAGON_TOKEN: 'a b', DRAGON_SESSION: 'c\nd' }, cfg())
68
+ expect(r.mode).toBe('none')
69
+ })
70
+ })
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Worker brain response normalization — the flaky fp8 tool-caller path the
3
+ * premortem flagged. The Cloudflare brain (Llama 3.3) returns inconsistent
4
+ * shapes; `normalizeWorkerTurn` must NEVER throw into the agent loop. It's pure,
5
+ * so every weird shape is pinned here.
6
+ *
7
+ * Runs against built output: `npm run build && vitest run`.
8
+ */
9
+ import { describe, it, expect } from 'vitest'
10
+ import { normalizeWorkerTurn } from '../dist/brain/worker.js'
11
+
12
+ describe('normalizeWorkerTurn — tolerant of fp8 weirdness', () => {
13
+ it('passes plain text through with no tool calls', () => {
14
+ const t = normalizeWorkerTurn({ response: 'hello' })
15
+ expect(t.text).toBe('hello')
16
+ expect(t.toolCalls).toEqual([])
17
+ })
18
+ it('parses string-encoded arguments into an object', () => {
19
+ const t = normalizeWorkerTurn({ tool_calls: [{ name: 'read_file', arguments: '{"path":"a.ts"}' }] })
20
+ expect(t.toolCalls[0]).toMatchObject({ id: 'wc_0', name: 'read_file', arguments: { path: 'a.ts' } })
21
+ })
22
+ it('keeps object arguments as-is', () => {
23
+ const t = normalizeWorkerTurn({ tool_calls: [{ name: 'grep', arguments: { pattern: 'x' } }] })
24
+ expect(t.toolCalls[0].arguments).toEqual({ pattern: 'x' })
25
+ })
26
+ it('coerces malformed/missing arguments to {} instead of throwing', () => {
27
+ const t = normalizeWorkerTurn({ tool_calls: [{ name: 'ls', arguments: 'not json' }, { name: 'pwd' }] })
28
+ expect(t.toolCalls[0].arguments).toEqual({})
29
+ expect(t.toolCalls[1].arguments).toEqual({})
30
+ })
31
+ it('drops nameless garbage tool_calls and re-indexes the survivors contiguously', () => {
32
+ const t = normalizeWorkerTurn({ tool_calls: [{ arguments: {} }, { name: 'glob', arguments: {} }] })
33
+ expect(t.toolCalls).toHaveLength(1)
34
+ expect(t.toolCalls[0]).toMatchObject({ id: 'wc_0', name: 'glob' })
35
+ })
36
+ it('handles a totally empty response without throwing', () => {
37
+ expect(normalizeWorkerTurn({})).toEqual({ text: '', toolCalls: [] })
38
+ })
39
+ })