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
@@ -0,0 +1,269 @@
1
+ /**
2
+ * dragon wyrm — Wyrm AI Memory System bridge.
3
+ *
4
+ * dragon wyrm status — health + stats from the running HTTP API
5
+ * dragon wyrm serve — start the Wyrm HTTP API on :3333
6
+ * dragon wyrm mcp — start the Wyrm MCP server over stdio
7
+ * dragon wyrm projects — list registered projects
8
+ * dragon wyrm scan <path> — discover git projects under <path>
9
+ * dragon wyrm search <q> — search across all memory
10
+ * dragon wyrm quests — list active quests
11
+ * dragon wyrm maintenance — vacuum + archive the database
12
+ *
13
+ * Endpoints + binary paths match Wyrm v5.x (packages/mcp-server). The
14
+ * earlier shape (`/api/health`, `npx wyrm`, `wyrm-deploy`) was written
15
+ * against a v3-era layout that no longer exists.
16
+ */
17
+
18
+ import type { Command } from 'commander'
19
+ import type { DragonConfig } from '../config.js'
20
+ import { getProductPath } from '../config.js'
21
+ import { run, label, success, error, info, warn, table } from '../utils.js'
22
+ import { existsSync, readFileSync } from 'node:fs'
23
+ import { join } from 'node:path'
24
+ import { homedir } from 'node:os'
25
+ import chalk from 'chalk'
26
+
27
+ function wyrmUrl(config: DragonConfig): string {
28
+ const port = config.products.wyrm?.port || 3333
29
+ return `http://127.0.0.1:${port}`
30
+ }
31
+
32
+ // The Wyrm HTTP server enforces Bearer auth. The raw key is shown once
33
+ // at `wyrm-setup` time and expected to be exported as WYRM_API_KEY. We
34
+ // also accept a key stashed in ~/.wyrm/api-key for convenience.
35
+ function wyrmToken(): string | null {
36
+ if (process.env.WYRM_API_KEY) return process.env.WYRM_API_KEY.trim()
37
+ const f = join(homedir(), '.wyrm', 'api-key')
38
+ if (existsSync(f)) {
39
+ try { return readFileSync(f, 'utf-8').trim() } catch { /* fall through */ }
40
+ }
41
+ return null
42
+ }
43
+
44
+ async function wyrmFetch<T = any>(url: string, init: RequestInit = {}, timeoutMs = 5000): Promise<T> {
45
+ const token = wyrmToken()
46
+ const headers: Record<string, string> = {
47
+ 'Accept': 'application/json',
48
+ ...(init.headers as Record<string, string> ?? {}),
49
+ }
50
+ if (token) headers['Authorization'] = `Bearer ${token}`
51
+ if (init.body && !headers['Content-Type']) headers['Content-Type'] = 'application/json'
52
+
53
+ const controller = new AbortController()
54
+ const timer = setTimeout(() => controller.abort(), timeoutMs)
55
+ try {
56
+ const res = await fetch(url, { ...init, headers, signal: controller.signal })
57
+ if (res.status === 401) {
58
+ throw new Error('401 unauthorized — set WYRM_API_KEY (`wyrm-setup` prints one on first run)')
59
+ }
60
+ if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`)
61
+ return res.json() as Promise<T>
62
+ } finally {
63
+ clearTimeout(timer)
64
+ }
65
+ }
66
+
67
+ // Resolve the runtime entry. Prefer the built file in the installed
68
+ // monorepo over `npx`, because Wyrm has no root package.json and
69
+ // `npx wyrm` will try to fetch from the public registry.
70
+ function wyrmEntry(path: string, name: 'http-server' | 'index'): string | null {
71
+ const candidates = [
72
+ join(path, 'packages', 'mcp-server', 'dist', `${name}.js`),
73
+ join(path, 'dist', `${name}.js`),
74
+ ]
75
+ return candidates.find(existsSync) ?? null
76
+ }
77
+
78
+ export function registerWyrmCommands(program: Command, config: DragonConfig) {
79
+ const wyrm = program
80
+ .command('wyrm')
81
+ .description('Wyrm — persistent AI memory (HTTP API + MCP server)')
82
+
83
+ // --- status ---
84
+ wyrm
85
+ .command('status')
86
+ .description('Check Wyrm server health & stats')
87
+ .action(async () => {
88
+ const url = wyrmUrl(config)
89
+ console.log(label('Wyrm'), `Probing ${chalk.dim(url)}\n`)
90
+ try {
91
+ const health = await wyrmFetch<any>(`${url}/health`)
92
+ let stats: any = {}
93
+ try { stats = await wyrmFetch<any>(`${url}/stats`) } catch { /* stats may need auth */ }
94
+ console.log(` Server: ${chalk.green('● online')} ${chalk.dim(`v${health.version ?? '?'}`)}`)
95
+ console.log(` Projects: ${stats.projects ?? chalk.dim('—')}`)
96
+ console.log(` Sessions: ${stats.sessions ?? chalk.dim('—')}`)
97
+ console.log(` Quests: ${stats.quests ?? chalk.dim('—')}`)
98
+ if (stats.data_entries !== undefined) {
99
+ console.log(` Data Lake: ${stats.data_entries} entries`)
100
+ }
101
+ console.log()
102
+ success('Wyrm is running')
103
+ } catch (e: any) {
104
+ error(`Wyrm not reachable — ${e.message}`)
105
+ info(`Start it with: ${chalk.green('dragon wyrm serve')}`)
106
+ process.exitCode = 1
107
+ }
108
+ })
109
+
110
+ // --- serve (HTTP API) ---
111
+ wyrm
112
+ .command('serve')
113
+ .description('Start the Wyrm HTTP API server')
114
+ .option('-p, --port <port>', 'Port number', '3333')
115
+ .action(async (opts) => {
116
+ const path = getProductPath(config, 'wyrm')
117
+ const entry = wyrmEntry(path, 'http-server')
118
+ if (!entry) {
119
+ error(`No built Wyrm at ${path}`)
120
+ info(`Run: ${chalk.green('dragon install wyrm')} first`)
121
+ process.exit(2)
122
+ }
123
+ console.log(label('Wyrm'), `Starting HTTP API on :${opts.port}…\n`)
124
+ const code = await run('node', [entry], path, { env: { ...process.env, WYRM_PORT: String(opts.port) } })
125
+ if (code !== 0) error(`Server exited with code ${code}`)
126
+ })
127
+
128
+ // --- mcp (stdio) ---
129
+ wyrm
130
+ .command('mcp')
131
+ .description('Start the Wyrm MCP server over stdio')
132
+ .action(async () => {
133
+ const path = getProductPath(config, 'wyrm')
134
+ const entry = wyrmEntry(path, 'index')
135
+ if (!entry) {
136
+ error(`No built Wyrm at ${path}`)
137
+ info(`Run: ${chalk.green('dragon install wyrm')} first`)
138
+ process.exit(2)
139
+ }
140
+ // Don't print a label — MCP clients read JSON-RPC from stdout.
141
+ const code = await run('node', [entry], path)
142
+ if (code !== 0) error(`MCP server exited with code ${code}`)
143
+ })
144
+
145
+ // --- projects ---
146
+ wyrm
147
+ .command('projects')
148
+ .description('List registered projects')
149
+ .action(async () => {
150
+ const url = wyrmUrl(config)
151
+ try {
152
+ const data = await wyrmFetch<any>(`${url}/projects`)
153
+ const list = data.projects ?? data ?? []
154
+ if (Array.isArray(list) && list.length) {
155
+ table(list.map((p: any) => ({
156
+ Name: p.name ?? p.id ?? '—',
157
+ Path: p.path ?? '—',
158
+ Language: p.language ?? '—',
159
+ Sessions: String(p.session_count ?? p.sessions ?? 0),
160
+ })))
161
+ } else {
162
+ info('No projects registered. Run: dragon wyrm scan <path>')
163
+ }
164
+ } catch (e: any) {
165
+ error(`Failed: ${e.message}`)
166
+ process.exitCode = 1
167
+ }
168
+ })
169
+
170
+ // --- scan ---
171
+ wyrm
172
+ .command('scan <path>')
173
+ .description('Scan directory for git projects')
174
+ .action(async (scanPath) => {
175
+ const url = wyrmUrl(config)
176
+ console.log(label('Wyrm'), `Scanning ${chalk.dim(scanPath)}…\n`)
177
+ try {
178
+ const data = await wyrmFetch<any>(`${url}/projects/scan`, {
179
+ method: 'POST',
180
+ body: JSON.stringify({ path: scanPath }),
181
+ })
182
+ success(`Found ${data.projects_found ?? data.found ?? 0} project(s)`)
183
+ } catch (e: any) {
184
+ error(`Failed: ${e.message}`)
185
+ process.exitCode = 1
186
+ }
187
+ })
188
+
189
+ // --- search ---
190
+ wyrm
191
+ .command('search <query>')
192
+ .description('Search across all memory')
193
+ .action(async (query) => {
194
+ const url = wyrmUrl(config)
195
+ try {
196
+ const data = await wyrmFetch<any>(`${url}/search?q=${encodeURIComponent(query)}`)
197
+ const results = data.results ?? data ?? []
198
+ if (Array.isArray(results) && results.length) {
199
+ for (const r of results) {
200
+ const snippet = (r.content ?? r.snippet ?? '').slice(0, 120)
201
+ console.log(` ${chalk.green(r.project ?? 'global')} ${chalk.dim('—')} ${snippet}`)
202
+ }
203
+ console.log(chalk.dim(`\n ${results.length} result(s)`))
204
+ } else {
205
+ info('No results')
206
+ }
207
+ } catch (e: any) {
208
+ error(`Failed: ${e.message}`)
209
+ process.exitCode = 1
210
+ }
211
+ })
212
+
213
+ // --- quests ---
214
+ wyrm
215
+ .command('quests')
216
+ .description('List active quests/tasks')
217
+ .option('-a, --all', 'Include completed quests')
218
+ .action(async (opts) => {
219
+ const url = wyrmUrl(config)
220
+ try {
221
+ const data = await wyrmFetch<any>(`${url}/quests${opts.all ? '?all=1' : ''}`)
222
+ const quests = data.quests ?? data ?? []
223
+ if (Array.isArray(quests) && quests.length) {
224
+ for (const q of quests) {
225
+ const icon = q.status === 'completed' ? chalk.green('✓') : chalk.dim('○')
226
+ console.log(` ${icon} ${q.title ?? q.name ?? q.id} ${chalk.dim(q.project ?? '')}`)
227
+ }
228
+ } else {
229
+ info('No active quests')
230
+ }
231
+ } catch (e: any) {
232
+ error(`Failed: ${e.message}`)
233
+ process.exitCode = 1
234
+ }
235
+ })
236
+
237
+ // --- maintenance ---
238
+ wyrm
239
+ .command('maintenance')
240
+ .description('Run database maintenance (vacuum, archive)')
241
+ .action(async () => {
242
+ const url = wyrmUrl(config)
243
+ console.log(label('Wyrm'), 'Running maintenance…\n')
244
+ try {
245
+ const data = await wyrmFetch<any>(`${url}/maintenance`, { method: 'POST' })
246
+ success(data.message ?? 'Maintenance complete')
247
+ } catch (e: any) {
248
+ error(`Failed: ${e.message}`)
249
+ process.exitCode = 1
250
+ }
251
+ })
252
+
253
+ // Default action — short summary instead of dumping help.
254
+ wyrm.action(() => {
255
+ const tokened = wyrmToken() !== null
256
+ console.log()
257
+ console.log(` ${label('Wyrm')} ${chalk.dim('persistent AI memory · HTTP :3333 · MCP stdio')}`)
258
+ console.log()
259
+ console.log(` ${chalk.dim('dragon wyrm status')} ${chalk.dim('— server health + stats')}`)
260
+ console.log(` ${chalk.dim('dragon wyrm serve')} ${chalk.dim('— start the HTTP API')}`)
261
+ console.log(` ${chalk.dim('dragon wyrm mcp')} ${chalk.dim('— start the MCP server (stdio)')}`)
262
+ console.log()
263
+ if (!tokened) {
264
+ warn('WYRM_API_KEY not set — auth-protected endpoints will 401')
265
+ info(`Print one with: ${chalk.green('cd ~/Git\\ Projects/Wyrm/packages/mcp-server && node dist/setup.js')}`)
266
+ }
267
+ console.log()
268
+ })
269
+ }
package/src/config.ts ADDED
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Dragon CLI configuration
3
+ * Stores product paths & settings in ~/.dragon/config.json
4
+ */
5
+
6
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from 'fs'
7
+ import { homedir } from 'os'
8
+ import { join } from 'path'
9
+
10
+ export interface DragonConfig {
11
+ products: {
12
+ scale: { path: string; url?: string }
13
+ wyrm: { path: string; port?: number }
14
+ pentest: { path: string; controlPort?: number; controlUiPort?: number }
15
+ keep: { path: string }
16
+ net: { path: string; apiPort?: number; uiPort?: number }
17
+ }
18
+ defaults: {
19
+ editor: string
20
+ }
21
+ /** Dragon assistant (account.ghosts.lk) auth for the hosted portal tool. */
22
+ auth?: {
23
+ apiBase?: string // override the assistant origin (default https://account.ghosts.lk)
24
+ token?: string // bearer personal-access-token (device-flow; forward-compatible)
25
+ session?: string // gp_session cookie value — works against prod today
26
+ email?: string // last verified identity, for display only
27
+ }
28
+ /** The agent's reasoning brain (tools always run locally; this is the model). */
29
+ brain?: {
30
+ provider?: 'claude' | 'openai' | 'local' | 'ghost' | 'worker' | 'custom' | 'router' // default 'claude'
31
+ model?: string // override hosted model id (claude/openai)
32
+ localModel?: string // Ollama model tag, e.g. 'qwen2.5-coder:7b'
33
+ localBaseURL?: string // default http://localhost:11434/v1
34
+ ghostModel?: string // DragonSpark tag (default 'dragonspark')
35
+ ghostBaseURL?: string // default http://localhost:11434/v1
36
+ customBaseURL?: string // any OpenAI-compatible endpoint (OpenRouter/vLLM/LM Studio)
37
+ customModel?: string // model id for the custom endpoint
38
+ keys?: { anthropic?: string; openai?: string } // env vars take precedence
39
+ // Ghost Router (provider 'router') role overrides — Ollama tags:
40
+ routerWorkhorse?: string // tool/agent turns (default 'mistral-nemo')
41
+ routerReasoner?: string // hard reasoning, no tools (default 'vibethinker')
42
+ routerCheap?: string // simple chat (default 'qwen2.5:1.5b')
43
+ }
44
+ /** Extra MCP servers wired into the agent (`dragon mcp add`). */
45
+ mcpServers?: Record<string, { command: string; args?: string[]; env?: Record<string, string> }>
46
+ }
47
+
48
+ const CONFIG_DIR = join(homedir(), '.dragon')
49
+ const CONFIG_FILE = join(CONFIG_DIR, 'config.json')
50
+
51
+ const DEFAULT_CONFIG: DragonConfig = {
52
+ products: {
53
+ scale: { path: '' },
54
+ wyrm: { path: '', port: 3333 },
55
+ pentest: { path: '', controlPort: 4091, controlUiPort: 4090 },
56
+ keep: { path: '' },
57
+ net: { path: '', apiPort: 4080, uiPort: 4081 },
58
+ },
59
+ defaults: {
60
+ editor: process.env.EDITOR || 'code',
61
+ },
62
+ }
63
+
64
+ export function loadConfig(): DragonConfig {
65
+ if (!existsSync(CONFIG_FILE)) {
66
+ return DEFAULT_CONFIG
67
+ }
68
+ try {
69
+ const raw = readFileSync(CONFIG_FILE, 'utf-8')
70
+ const merged = { ...DEFAULT_CONFIG, ...JSON.parse(raw) }
71
+ // Deep-merge products so new keys appear with defaults
72
+ merged.products = { ...DEFAULT_CONFIG.products, ...(merged.products || {}) }
73
+ return merged
74
+ } catch {
75
+ return DEFAULT_CONFIG
76
+ }
77
+ }
78
+
79
+ export function saveConfig(config: DragonConfig): void {
80
+ // The config may hold a bearer token / session — lock it down on EVERY write
81
+ // path (not just after login), so it's never briefly world-readable.
82
+ mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 })
83
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + '\n', { mode: 0o600 })
84
+ try { chmodSync(CONFIG_FILE, 0o600) } catch { /* best-effort on odd filesystems */ }
85
+ }
86
+
87
+ export function getProductPath(config: DragonConfig, product: keyof DragonConfig['products']): string {
88
+ const p = config.products[product].path
89
+ if (!p) {
90
+ throw new Error(`${product} path not configured. Run: dragon init`)
91
+ }
92
+ return p
93
+ }
package/src/index.ts ADDED
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Dragon CLI — Unified management for Ghost Protocol products.
4
+ *
5
+ * dragon list — print the stack + live install / running state
6
+ * dragon up — clone + install every product (fresh-machine bootstrap)
7
+ * dragon install — install one product
8
+ * dragon update — git pull every product
9
+ * dragon serve — boot the dragon stack daemons
10
+ * dragon down — stop everything
11
+ * dragon doctor — quick health check across the stack
12
+ *
13
+ * dragon pentest|osint|memory|keep|wyrm|scale — per-product CLIs
14
+ */
15
+
16
+ import { Command } from 'commander'
17
+ import chalk from 'chalk'
18
+ import { C } from './utils.js'
19
+ import { brandHeader } from './ui.js'
20
+ import { loadConfig } from './config.js'
21
+ import { registerScaleCommands } from './commands/scale.js'
22
+ import { registerWyrmCommands } from './commands/wyrm.js'
23
+ import { registerPentestCommands } from './commands/pentest.js'
24
+ import { registerKeepCommands } from './commands/keep.js'
25
+ import { registerOsintCommands } from './commands/osint.js'
26
+ import { registerMemoryCommands } from './commands/memory.js'
27
+ import { registerServeCommands } from './commands/serve.js'
28
+ import { registerAlertsCommands } from './commands/alerts.js'
29
+ import { registerAiCommands } from './commands/ai.js'
30
+ import { registerChatCommands } from './commands/chat.js'
31
+ import { registerLoginCommands } from './commands/login.js'
32
+ import { registerConfigCommands } from './commands/config.js'
33
+ import { registerDoctorCommands } from './commands/doctor.js'
34
+ import { registerMcpCommands } from './commands/mcp.js'
35
+ import { registerMaintenanceCommands } from './commands/maintenance.js'
36
+ import { registerTuiCommands } from './commands/tui.js'
37
+ import { registerBillingCommands } from './commands/billing.js'
38
+ import { registerLifecycleCommands } from './commands/lifecycle.js'
39
+ import { registerGlobalCommands } from './commands/global.js'
40
+
41
+ const VERSION = '4.2.1'
42
+
43
+ // Ops-console brand splash — framed panel + chrome wordmark (see src/ui.ts).
44
+ const HEADER = brandHeader(VERSION)
45
+
46
+ const program = new Command()
47
+
48
+ program
49
+ .name('dragon')
50
+ .description('Ghost Protocol — unified stack control')
51
+ .version(VERSION, '-v, --version')
52
+ .option('--debug', 'print full stack traces on error')
53
+ .addHelpText('beforeAll', `\n${HEADER}`)
54
+ .addHelpText('afterAll',
55
+ `\n ${chalk.dim('Tip:')} run ${chalk.green('dragon list')} to see what's installed,` +
56
+ `\n ${chalk.green('dragon up')} to bootstrap a fresh machine.\n`)
57
+
58
+ const config = loadConfig()
59
+
60
+ registerScaleCommands(program, config)
61
+ registerWyrmCommands(program, config)
62
+ registerPentestCommands(program, config)
63
+ registerKeepCommands(program, config)
64
+ registerOsintCommands(program, config)
65
+ registerMemoryCommands(program, config)
66
+ registerServeCommands(program, config)
67
+ registerAlertsCommands(program, config)
68
+ registerAiCommands(program, config)
69
+ registerChatCommands(program, config)
70
+ registerLoginCommands(program, config)
71
+ registerConfigCommands(program, config)
72
+ registerDoctorCommands(program, config)
73
+ registerMcpCommands(program, config)
74
+ registerMaintenanceCommands(program, config)
75
+ registerTuiCommands(program, config)
76
+ registerBillingCommands(program, config)
77
+ registerLifecycleCommands(program, config)
78
+ registerGlobalCommands(program, config)
79
+
80
+ // ── Global error safety net — never dump a raw stack on the operator. ──
81
+ const DEBUG = process.argv.includes('--debug') || !!process.env.DRAGON_DEBUG
82
+ function fatal(label: string, err: unknown): never {
83
+ const msg = err instanceof Error ? err.message : String(err)
84
+ process.stderr.write(`\n ${C.critical('⚠')} ${label}: ${msg}\n`)
85
+ if (DEBUG && err instanceof Error && err.stack) process.stderr.write(C.faint(err.stack) + '\n')
86
+ else process.stderr.write(C.faint(' run with --debug (or DRAGON_DEBUG=1) for the full trace\n'))
87
+ process.exit(1)
88
+ }
89
+ process.on('uncaughtException', (e) => fatal('unexpected error', e))
90
+ process.on('unhandledRejection', (e) => fatal('unhandled rejection', e))
91
+
92
+ program.parseAsync().catch((e) => fatal('command failed', e))
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Dragon Stack manifest loader.
3
+ *
4
+ * Reads dragon-manifest.toml at install time so `dragon up`,
5
+ * `dragon update`, `dragon install`, `dragon stop --all` know the
6
+ * full product portfolio.
7
+ *
8
+ * Copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
9
+ */
10
+
11
+ import { readFileSync, existsSync } from 'node:fs'
12
+ import { homedir } from 'node:os'
13
+ import { join, resolve } from 'node:path'
14
+ import { fileURLToPath } from 'node:url'
15
+
16
+ export interface ManifestProduct {
17
+ key: string
18
+ name: string
19
+ repo: string
20
+ dir: string
21
+ stack: 'operator' | 'commercial' | 'daemon' | 'library'
22
+ kind: 'rust' | 'node' | 'python' | 'monorepo' | 'docker'
23
+ install: string[]
24
+ start?: string
25
+ stop?: string
26
+ port?: number
27
+ requires?: string[]
28
+ }
29
+
30
+ export interface Manifest {
31
+ manifest_version: string
32
+ default_root: string
33
+ product: ManifestProduct[]
34
+ }
35
+
36
+ // Minimal TOML reader — handles the subset we use here so the CLI
37
+ // doesn't take on a TOML dep. Spec: `[[product]]` arrays, `key = "value"`
38
+ // strings, `key = ["a", "b"]` string arrays, `key = 123` numbers,
39
+ // comment lines starting with `#`.
40
+ function parseToml(text: string): Manifest {
41
+ const out: any = { product: [] }
42
+ let cur: any | null = null
43
+ let inArray = false
44
+ for (const rawLine of text.split(/\r?\n/)) {
45
+ const line = rawLine.replace(/#.*$/, '').trim()
46
+ if (!line) continue
47
+ if (line === '[[product]]') {
48
+ inArray = true
49
+ cur = {}
50
+ out.product.push(cur)
51
+ continue
52
+ }
53
+ if (line.startsWith('[') && line.endsWith(']')) {
54
+ inArray = false
55
+ cur = null
56
+ continue
57
+ }
58
+ const eq = line.indexOf('=')
59
+ if (eq < 0) continue
60
+ const key = line.slice(0, eq).trim()
61
+ let val: any = line.slice(eq + 1).trim()
62
+ if (val.startsWith('[') && val.endsWith(']')) {
63
+ val = val
64
+ .slice(1, -1)
65
+ .split(',')
66
+ .map((s: string) => s.trim().replace(/^"|"$/g, ''))
67
+ .filter(Boolean)
68
+ } else if (val.startsWith('"') && val.endsWith('"')) {
69
+ val = val.slice(1, -1)
70
+ } else if (!isNaN(Number(val))) {
71
+ val = Number(val)
72
+ }
73
+ if (inArray && cur) cur[key] = val
74
+ else out[key] = val
75
+ }
76
+ return out as Manifest
77
+ }
78
+
79
+ const SELF_DIR = fileURLToPath(new URL('..', import.meta.url))
80
+ const CANDIDATES = [
81
+ join(homedir(), '.dragon', 'manifest.toml'),
82
+ resolve(SELF_DIR, '..', 'dragon-manifest.toml'),
83
+ resolve(homedir(), 'Git Projects', 'dragon-cli', 'dragon-manifest.toml'),
84
+ ]
85
+
86
+ export function loadManifest(): Manifest {
87
+ for (const p of CANDIDATES) {
88
+ if (existsSync(p)) {
89
+ return parseToml(readFileSync(p, 'utf-8'))
90
+ }
91
+ }
92
+ throw new Error(
93
+ `dragon-manifest.toml not found. Looked in:\n ${CANDIDATES.join('\n ')}`,
94
+ )
95
+ }
96
+
97
+ export function resolveRoot(m: Manifest): string {
98
+ const r = m.default_root.replace(/^~/, homedir())
99
+ return r
100
+ }
101
+
102
+ export function productPath(m: Manifest, p: ManifestProduct): string {
103
+ return join(resolveRoot(m), p.dir)
104
+ }