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,137 @@
1
+ /**
2
+ * dragon config — inspect + set the agent's brain and credentials.
3
+ *
4
+ * dragon config show
5
+ * dragon config brain claude|openai|local
6
+ * dragon config model claude-sonnet-4-6
7
+ * dragon config local-model qwen2.5-coder:7b
8
+ * dragon config key anthropic sk-ant-… (stored in ~/.dragon/config.json, 0600)
9
+ *
10
+ * Env vars (ANTHROPIC_API_KEY / OPENAI_API_KEY / DRAGON_BRAIN / DRAGON_MODEL)
11
+ * always take precedence over stored config.
12
+ *
13
+ * Copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
14
+ */
15
+
16
+ import type { Command } from 'commander'
17
+ import { loadConfig, saveConfig, type DragonConfig } from '../config.js'
18
+ import { resolveProvider, PROVIDERS } from '../brain/index.js'
19
+ import { resolveAuth } from '../auth.js'
20
+ import { C, success, error } from '../utils.js'
21
+ import { panel, chrome, statusDot } from '../ui.js'
22
+ import { chmodSync, existsSync } from 'node:fs'
23
+ import { homedir } from 'node:os'
24
+ import { join } from 'node:path'
25
+
26
+ function lock(): void {
27
+ try { chmodSync(join(homedir(), '.dragon', 'config.json'), 0o600) } catch { /* best-effort */ }
28
+ }
29
+
30
+ function redact(v?: string): string {
31
+ if (!v) return C.faint('—')
32
+ return C.accent(`set (…${v.slice(-4)})`)
33
+ }
34
+
35
+ export function registerConfigCommands(program: Command, _config: DragonConfig) {
36
+ const cfg = program.command('config').description('Inspect + set the Dragon agent brain + credentials')
37
+
38
+ cfg
39
+ .command('show')
40
+ .description('Show current config (secrets redacted)')
41
+ .action(() => {
42
+ const c = loadConfig()
43
+ const a = resolveAuth()
44
+ console.log()
45
+ console.log(` ${C.accent.bold('brain')}`)
46
+ console.log(` provider: ${C.info(resolveProvider())} ${C.faint('(default')} ${C.faint(c.brain?.provider ?? 'claude')}${C.faint(')')}`)
47
+ console.log(` model: ${C.info(c.brain?.model ?? C.faint('default'))}`)
48
+ console.log(` local model: ${C.info(c.brain?.localModel ?? 'qwen2.5-coder:7b')} ${C.faint(c.brain?.localBaseURL ?? 'http://localhost:11434/v1')}`)
49
+ console.log(` keys: anthropic ${redact(process.env.ANTHROPIC_API_KEY ?? c.brain?.keys?.anthropic)} openai ${redact(process.env.OPENAI_API_KEY ?? c.brain?.keys?.openai)}`)
50
+ console.log(` ${C.accent.bold('portal')} (account.ghosts.lk)`)
51
+ console.log(` endpoint: ${C.info(a.apiBase)}`)
52
+ console.log(` auth: ${a.mode === 'none' ? C.faint('none') : C.accent(a.mode)}${a.email ? C.faint(' · ' + a.email) : ''}`)
53
+ console.log()
54
+ })
55
+
56
+ cfg
57
+ .command('brain <provider>')
58
+ .description(`Set the default reasoning brain: ${PROVIDERS.join(' | ')}`)
59
+ .action((provider: string) => {
60
+ const p = provider.toLowerCase()
61
+ if (!(PROVIDERS as string[]).includes(p)) { error(`unknown provider "${provider}". One of: ${PROVIDERS.join(', ')}`); process.exitCode = 1; return }
62
+ const c = loadConfig()
63
+ c.brain = { ...(c.brain ?? {}), provider: p as 'claude' | 'openai' | 'local' | 'ghost' | 'worker' | 'custom' }
64
+ saveConfig(c)
65
+ success(`default brain → ${p}`)
66
+ })
67
+
68
+ cfg
69
+ .command('model <id>')
70
+ .description('Set the hosted model id (claude/openai), e.g. claude-sonnet-4-6')
71
+ .action((id: string) => {
72
+ const c = loadConfig()
73
+ c.brain = { ...(c.brain ?? {}), model: id }
74
+ saveConfig(c)
75
+ success(`model → ${id}`)
76
+ })
77
+
78
+ cfg
79
+ .command('local-model <tag>')
80
+ .description('Set the local (Ollama) model tag, e.g. qwen2.5-coder:7b')
81
+ .action((tag: string) => {
82
+ const c = loadConfig()
83
+ c.brain = { ...(c.brain ?? {}), localModel: tag }
84
+ saveConfig(c)
85
+ success(`local model → ${tag}`)
86
+ })
87
+
88
+ cfg
89
+ .command('key <provider> <key>')
90
+ .description('Store an API key (anthropic | openai) in ~/.dragon/config.json (0600)')
91
+ .action((provider: string, key: string) => {
92
+ const p = provider.toLowerCase()
93
+ if (p !== 'anthropic' && p !== 'openai') { error('provider must be "anthropic" or "openai"'); process.exitCode = 1; return }
94
+ const c = loadConfig()
95
+ c.brain = { ...(c.brain ?? {}), keys: { ...(c.brain?.keys ?? {}), [p]: key } }
96
+ saveConfig(c)
97
+ lock()
98
+ success(`${p} key stored (config locked to 0600). Env vars still override.`)
99
+ })
100
+
101
+ cfg
102
+ .command('custom-url <url>')
103
+ .description('Set a custom OpenAI-compatible endpoint (OpenRouter / vLLM / LM Studio)')
104
+ .action((url: string) => { const c = loadConfig(); c.brain = { ...(c.brain ?? {}), customBaseURL: url }; saveConfig(c); success(`custom endpoint → ${url} (use: dragon config brain custom)`) })
105
+
106
+ cfg
107
+ .command('custom-model <id>')
108
+ .description('Set the model id for the custom endpoint')
109
+ .action((id: string) => { const c = loadConfig(); c.brain = { ...(c.brain ?? {}), customModel: id }; saveConfig(c); success(`custom model → ${id}`) })
110
+
111
+ // `dragon brains` — the model picker, as a command.
112
+ program
113
+ .command('brains')
114
+ .description('List reasoning brains + which are ready (the model picker)')
115
+ .action(() => {
116
+ const c = loadConfig()
117
+ const a = resolveAuth()
118
+ const active = resolveProvider()
119
+ const hasA = !!(process.env.ANTHROPIC_API_KEY || c.brain?.keys?.anthropic)
120
+ const hasO = !!(process.env.OPENAI_API_KEY || c.brain?.keys?.openai)
121
+ const hasC = !!(process.env.DRAGON_OPENAI_BASE || c.brain?.customBaseURL)
122
+ const ollama = ['/usr/local/bin/ollama', '/usr/bin/ollama', join(homedir(), '.local/bin/ollama')].some((p) => existsSync(p))
123
+ const rows: [string, boolean, string, string][] = [
124
+ ['claude', hasA, 'best for code', hasA ? '' : 'set ANTHROPIC_API_KEY'],
125
+ ['worker', a.mode !== 'none', 'free · our Cloudflare', a.mode !== 'none' ? '' : 'run `dragon login`'],
126
+ ['local', ollama, 'Ollama · private · free', ollama ? 'pull a model' : 'install Ollama'],
127
+ ['ghost', false, 'DragonSpark · our nano LLM', 'train + serve it'],
128
+ ['openai', hasO, 'GPT', hasO ? '' : 'set OPENAI_API_KEY'],
129
+ ['custom', hasC, 'any OpenAI-compatible endpoint', hasC ? '' : 'dragon config custom-url <url>'],
130
+ ]
131
+ const lines = rows.map(([id, ready, desc, need]) =>
132
+ `${id === active ? C.accent('▸') : ' '} ${statusDot(ready)} ${C.info(id.padEnd(7))} ${C.faint(desc)}${need ? C.faint(' · ' + need) : ''}`)
133
+ console.log()
134
+ console.log(panel(lines, { title: chrome('BRAINS') }))
135
+ console.log(` ${C.faint('active:')} ${C.accent(active)} ${C.faint('switch:')} dragon config brain <name> ${C.faint('per-run:')} dragon chat --brain <name>`)
136
+ })
137
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * dragon doctor — one-glance health check of everything the agent depends on:
3
+ * node, the active brain + readiness, auth, Wyrm memory, Ollama, the skill library,
4
+ * disk, and config-file permissions. Read-only; green/amber/red per check.
5
+ *
6
+ * Copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
7
+ */
8
+
9
+ import type { Command } from 'commander'
10
+ import { loadConfig, type DragonConfig } from '../config.js'
11
+ import { C } from '../utils.js'
12
+ import { panel, chrome } from '../ui.js'
13
+ import { resolveAuth, whoami } from '../auth.js'
14
+ import { resolveProvider } from '../brain/index.js'
15
+ import { Wyrm } from '../wyrm/mcp.js'
16
+ import { loadSkillLibrary } from '../agent/skills.js'
17
+ import { existsSync, statSync, statfsSync } from 'node:fs'
18
+ import { homedir } from 'node:os'
19
+ import { join } from 'node:path'
20
+
21
+ type Status = 'ok' | 'warn' | 'fail'
22
+ const mark = (s: Status) => (s === 'ok' ? C.accent('●') : s === 'warn' ? C.high('◐') : C.critical('○'))
23
+ const paint = (s: Status, v: string) => (s === 'fail' ? C.critical(v) : s === 'warn' ? C.high(v) : C.info(v))
24
+
25
+ export function registerDoctorCommands(program: Command, _config: DragonConfig) {
26
+ program
27
+ .command('doctor')
28
+ .description('Health check — brain, auth, memory, models, disk, perms')
29
+ .action(async () => {
30
+ const rows: [Status, string, string][] = []
31
+ const cfg = loadConfig()
32
+ const a = resolveAuth()
33
+ const ollama = ['/usr/local/bin/ollama', '/usr/bin/ollama', join(homedir(), '.local/bin/ollama')].some(existsSync)
34
+
35
+ const major = Number(process.version.slice(1).split('.')[0])
36
+ rows.push([major >= 20 ? 'ok' : 'warn', 'node', `${process.version}${major >= 20 ? '' : ' (20+ recommended)'}`])
37
+
38
+ const active = resolveProvider()
39
+ const ready: Record<string, boolean> = {
40
+ claude: !!(process.env.ANTHROPIC_API_KEY || cfg.brain?.keys?.anthropic),
41
+ worker: a.mode !== 'none',
42
+ local: ollama,
43
+ openai: !!(process.env.OPENAI_API_KEY || cfg.brain?.keys?.openai),
44
+ custom: !!(process.env.DRAGON_OPENAI_BASE || cfg.brain?.customBaseURL),
45
+ ghost: false,
46
+ }
47
+ const brainOk = ready[active] ?? false
48
+ rows.push([brainOk ? 'ok' : 'warn', 'brain', `${active}${brainOk ? ' · ready' : ' · not set up (will fall back)'}`])
49
+
50
+ if (a.mode === 'none') rows.push(['warn', 'auth', 'not signed in — run `dragon login`'])
51
+ else { const me = await whoami(); rows.push([me.ok ? 'ok' : 'fail', 'auth', me.ok ? `${a.mode} · ${me.email ?? 'verified'}` : `${a.mode} · invalid (re-login)`]) }
52
+
53
+ const w = new Wyrm()
54
+ const wok = await w.connect()
55
+ rows.push([wok ? 'ok' : 'warn', 'memory', wok ? `Wyrm · ${w.toolSpecs().length} tools` : 'Wyrm unavailable (agent works without it)'])
56
+ await w.close()
57
+
58
+ rows.push([ollama ? 'ok' : 'warn', 'ollama', ollama ? 'installed' : 'not found (local brain needs it)'])
59
+ const sk = loadSkillLibrary()
60
+ rows.push([sk.count > 0 ? 'ok' : 'warn', 'skills', `${sk.count} loaded`])
61
+
62
+ try { const st = statfsSync(process.cwd()); const g = (st.bavail * st.bsize) / 1e9; rows.push([g > 2 ? 'ok' : g > 0.5 ? 'warn' : 'fail', 'disk', `${g.toFixed(1)} GB free`]) } catch { /* skip */ }
63
+ try { const m = statSync(join(homedir(), '.dragon', 'config.json')).mode & 0o777; rows.push([m === 0o600 ? 'ok' : 'warn', 'config', `~/.dragon/config.json ${m.toString(8)}${m === 0o600 ? '' : ' (expected 600)'}`]) } catch { /* no config yet */ }
64
+
65
+ console.log()
66
+ console.log(panel(rows.map(([s, k, v]) => `${mark(s)} ${C.faint(k.padEnd(7))} ${paint(s, v)}`), { title: chrome('DOCTOR') }))
67
+ const fails = rows.filter((r) => r[0] === 'fail').length
68
+ const warns = rows.filter((r) => r[0] === 'warn').length
69
+ console.log(` ${fails ? C.critical(`${fails} failing`) : warns ? C.high(`${warns} warning${warns > 1 ? 's' : ''}`) : C.accent('all systems green')}`)
70
+ })
71
+ }
@@ -0,0 +1,256 @@
1
+ /**
2
+ * dragon init / dragon status / dragon doctor — Global commands
3
+ *
4
+ * - init: Auto-detect product paths and write ~/.dragon/config.json
5
+ * - status: Health check across all products + APIs
6
+ * - doctor: Contract-test the cross-tool flows (lightweight)
7
+ */
8
+
9
+ import type { Command } from 'commander'
10
+ import type { DragonConfig } from '../config.js'
11
+ import { saveConfig } from '../config.js'
12
+ import { label, success, warn } from '../utils.js'
13
+ import chalk from 'chalk'
14
+ import { existsSync, statSync } from 'fs'
15
+ import { join, resolve } from 'path'
16
+ import { homedir } from 'os'
17
+
18
+ const KNOWN_MARKERS: Record<string, { files: string[]; name: string }> = {
19
+ scale: { files: ['includes/payment.php', 'admin/orders.php', 'api/health.php'], name: 'DragonScale' },
20
+ wyrm: { files: ['packages/mcp-server/src/index.ts', 'install.sh'], name: 'Wyrm' },
21
+ pentest: { files: ['phantomdragon.py', 'phantom_dragon_ai/control_api.py'], name: 'PhantomDragon' },
22
+ keep: { files: ['src/engine/sentinel.rs', 'src/engine/shield.rs'], name: 'DragonKeep' },
23
+ net: { files: ['packages/api/src/server.ts', 'pnpm-workspace.yaml'], name: 'DragonNet' },
24
+ }
25
+
26
+ function detectProduct(dir: string, product: string): boolean {
27
+ const markers = KNOWN_MARKERS[product]
28
+ if (!markers) return false
29
+ return markers.files.some(f => existsSync(join(dir, f)))
30
+ }
31
+
32
+ async function ping(url: string, timeoutMs = 1500): Promise<{ ok: boolean; status?: number; data?: any; error?: string }> {
33
+ try {
34
+ const controller = new AbortController()
35
+ const timer = setTimeout(() => controller.abort(), timeoutMs)
36
+ try {
37
+ const res = await fetch(url, { signal: controller.signal })
38
+ const text = await res.text()
39
+ let parsed: any = null
40
+ try { parsed = JSON.parse(text) } catch { parsed = text }
41
+ return { ok: res.ok, status: res.status, data: parsed }
42
+ } finally { clearTimeout(timer) }
43
+ } catch (e) {
44
+ return { ok: false, error: String(e) }
45
+ }
46
+ }
47
+
48
+ export function registerGlobalCommands(program: Command, config: DragonConfig) {
49
+
50
+ // --- init ---
51
+ program
52
+ .command('init')
53
+ .description('Auto-detect products and configure dragon CLI')
54
+ .option('-d, --dir <dir>', 'Directory to scan', resolve('.'))
55
+ .action(async (opts) => {
56
+ console.log(label('Dragon'), 'Initializing...\n')
57
+
58
+ const scanDirs = [
59
+ opts.dir,
60
+ join(homedir(), 'Git Projects'),
61
+ join(homedir(), 'Projects'),
62
+ join(homedir(), 'src'),
63
+ ]
64
+
65
+ const found: Record<string, string> = {}
66
+
67
+ for (const baseDir of scanDirs) {
68
+ if (!existsSync(baseDir)) continue
69
+ const { readdirSync } = await import('fs')
70
+ const entries = readdirSync(baseDir, { withFileTypes: true })
71
+
72
+ for (const entry of entries) {
73
+ if (!entry.isDirectory()) continue
74
+ const fullPath = join(baseDir, entry.name)
75
+
76
+ for (const [product, _] of Object.entries(KNOWN_MARKERS)) {
77
+ if (!found[product] && detectProduct(fullPath, product)) {
78
+ found[product] = fullPath
79
+ }
80
+ }
81
+ }
82
+ }
83
+
84
+ // Update config
85
+ for (const [product, path] of Object.entries(found)) {
86
+ (config.products as any)[product].path = path
87
+ success(`${KNOWN_MARKERS[product].name} → ${chalk.dim(path)}`)
88
+ }
89
+
90
+ for (const [product, markers] of Object.entries(KNOWN_MARKERS)) {
91
+ if (!found[product]) {
92
+ warn(`${markers.name} not found`)
93
+ }
94
+ }
95
+
96
+ saveConfig(config)
97
+ console.log()
98
+ success(`Config saved to ${chalk.dim('~/.dragon/config.json')}`)
99
+ })
100
+
101
+ // --- status ---
102
+ program
103
+ .command('status')
104
+ .description('Health check across all products + APIs')
105
+ .action(async () => {
106
+ console.log(label('Dragon'), 'System status:\n')
107
+
108
+ // DragonScale
109
+ const scaleUrl = config.products.scale.url
110
+ const scalePath = config.products.scale.path
111
+ if (scalePath && existsSync(join(scalePath, 'index.php'))) {
112
+ console.log(` ${chalk.green('●')} DragonScale ${chalk.dim(scaleUrl || scalePath)} ${chalk.green('Installed')}`)
113
+ } else {
114
+ console.log(` ${chalk.dim('○')} DragonScale ${chalk.dim('Not configured')}`)
115
+ }
116
+
117
+ // Wyrm
118
+ const wyrmPath = config.products.wyrm.path
119
+ const wyrmPort = config.products.wyrm.port || 3333
120
+ if (wyrmPath) {
121
+ const r = await ping(`http://localhost:${wyrmPort}/health`)
122
+ if (r.ok) {
123
+ console.log(` ${chalk.green('●')} Wyrm ${chalk.dim(`localhost:${wyrmPort}`)} ${chalk.green('Online')}`)
124
+ } else {
125
+ console.log(` ${chalk.yellow('●')} Wyrm ${chalk.dim(`localhost:${wyrmPort}`)} ${chalk.yellow('Installed · Offline')}`)
126
+ }
127
+ } else {
128
+ console.log(` ${chalk.dim('○')} Wyrm ${chalk.dim('Not configured')}`)
129
+ }
130
+
131
+ // PhantomDragon engine + control_api
132
+ const pentestPath = config.products.pentest.path
133
+ if (pentestPath && (existsSync(join(pentestPath, 'phantom_dragon_ai/control_api.py'))
134
+ || existsSync(join(pentestPath, 'phantomdragon.py')))) {
135
+ console.log(` ${chalk.green('●')} PhantomDragon engine ${chalk.dim(pentestPath)} ${chalk.green('Installed')}`)
136
+ const apiPort = config.products.pentest.controlPort ?? 4091
137
+ const uiPort = config.products.pentest.controlUiPort ?? 4090
138
+ const apiR = await ping(`http://localhost:${apiPort}/health`)
139
+ if (apiR.ok && apiR.data) {
140
+ const d = apiR.data as any
141
+ console.log(` ${chalk.green('●')} Control API ${chalk.dim(`localhost:${apiPort}`)} ${chalk.green('Online')} ${chalk.dim(`(${d.scan_count} scans, ${d.active_runs} runs)`)}`)
142
+ } else {
143
+ console.log(` ${chalk.yellow('●')} Control API ${chalk.dim(`localhost:${apiPort}`)} ${chalk.yellow('Offline')}`)
144
+ }
145
+ const uiR = await ping(`http://localhost:${uiPort}/`)
146
+ if (uiR.ok) {
147
+ console.log(` ${chalk.green('●')} Control Dashboard ${chalk.dim(`localhost:${uiPort}`)} ${chalk.green('Online')}`)
148
+ } else {
149
+ console.log(` ${chalk.yellow('●')} Control Dashboard ${chalk.dim(`localhost:${uiPort}`)} ${chalk.yellow('Offline')}`)
150
+ }
151
+ } else {
152
+ console.log(` ${chalk.dim('○')} PhantomDragon engine ${chalk.dim('Not configured')}`)
153
+ }
154
+
155
+ // DragonNet
156
+ const netPath = config.products.net.path
157
+ if (netPath && existsSync(join(netPath, 'pnpm-workspace.yaml'))) {
158
+ console.log(` ${chalk.green('●')} DragonNet ${chalk.dim(netPath)} ${chalk.green('Installed')}`)
159
+ const apiPort = config.products.net.apiPort ?? 4080
160
+ const uiPort = config.products.net.uiPort ?? 4081
161
+ const apiR = await ping(`http://localhost:${apiPort}/health`)
162
+ if (apiR.ok) {
163
+ console.log(` ${chalk.green('●')} OSINT API ${chalk.dim(`localhost:${apiPort}`)} ${chalk.green('Online')}`)
164
+ } else {
165
+ console.log(` ${chalk.yellow('●')} OSINT API ${chalk.dim(`localhost:${apiPort}`)} ${chalk.yellow('Offline')}`)
166
+ }
167
+ const uiR = await ping(`http://localhost:${uiPort}/`)
168
+ if (uiR.ok) {
169
+ console.log(` ${chalk.green('●')} OSINT Dashboard ${chalk.dim(`localhost:${uiPort}`)} ${chalk.green('Online')}`)
170
+ } else {
171
+ console.log(` ${chalk.yellow('●')} OSINT Dashboard ${chalk.dim(`localhost:${uiPort}`)} ${chalk.yellow('Offline')}`)
172
+ }
173
+ } else {
174
+ console.log(` ${chalk.dim('○')} DragonNet ${chalk.dim('Not configured')}`)
175
+ }
176
+
177
+ // Phantom Memory
178
+ const memPath = join(homedir(), '.copilot', 'phantom-memory.db')
179
+ if (existsSync(memPath)) {
180
+ const bytes = statSync(memPath).size
181
+ const apiPort = config.products.pentest.controlPort ?? 4091
182
+ const memR = await ping(`http://localhost:${apiPort}/v1/memory`)
183
+ let targetCount = '—'
184
+ if (memR.ok && memR.data?.targets) targetCount = String(memR.data.targets.length)
185
+ const sizeStr = bytes < 1024*1024 ? `${(bytes/1024).toFixed(1)} KB` : `${(bytes/1024/1024).toFixed(1)} MB`
186
+ console.log(` ${chalk.green('●')} Phantom Memory ${chalk.dim(memPath)} ${chalk.green(`${targetCount} targets · ${sizeStr}`)}`)
187
+ } else {
188
+ console.log(` ${chalk.dim('○')} Phantom Memory ${chalk.dim('No DB yet — runs auto-create on first scan')}`)
189
+ }
190
+
191
+ // DragonKeep (Rust defensive backbone)
192
+ const keepPath = config.products.keep.path
193
+ if (keepPath && existsSync(join(keepPath, 'Cargo.toml'))) {
194
+ const dbgBin = join(keepPath, 'target/debug/dragonkeep')
195
+ const relBin = join(keepPath, 'target/release/dragonkeep')
196
+ const built = existsSync(relBin) || existsSync(dbgBin)
197
+ const label_ = built ? chalk.green('Installed · built') : chalk.yellow('Installed · not yet built')
198
+ console.log(` ${chalk.green('●')} DragonKeep ${chalk.dim(keepPath)} ${label_}`)
199
+ if (!built) {
200
+ console.log(` ${chalk.dim('→ cargo build --release in ' + keepPath)}`)
201
+ }
202
+ } else {
203
+ console.log(` ${chalk.dim('○')} DragonKeep ${chalk.dim('Not configured')}`)
204
+ }
205
+ console.log()
206
+ })
207
+
208
+ // --- contract (cross-tool flow checks; `dragon doctor` is the health panel) ---
209
+ program
210
+ .command('contract')
211
+ .description('Contract-test cross-tool flows (Phantom Memory / Control API / DragonNet / Wyrm)')
212
+ .action(async () => {
213
+ console.log(label('Dragon'), 'Contract checks:\n')
214
+
215
+ const checks: Array<{ name: string; ok: boolean; note?: string }> = []
216
+
217
+ // Phantom Memory exists?
218
+ const memPath = join(homedir(), '.copilot', 'phantom-memory.db')
219
+ checks.push({ name: 'Phantom Memory DB present', ok: existsSync(memPath), note: memPath })
220
+
221
+ // Control API speaks memory?
222
+ const apiPort = config.products.pentest.controlPort ?? 4091
223
+ const memR = await ping(`http://localhost:${apiPort}/v1/memory`)
224
+ checks.push({
225
+ name: 'Control API /v1/memory reachable',
226
+ ok: memR.ok,
227
+ note: memR.ok ? `${memR.data?.targets?.length ?? 0} targets` : memR.error,
228
+ })
229
+
230
+ // DragonNet API reachable?
231
+ const dnPort = config.products.net.apiPort ?? 4080
232
+ const dnR = await ping(`http://localhost:${dnPort}/health`)
233
+ checks.push({
234
+ name: 'DragonNet /health reachable',
235
+ ok: dnR.ok,
236
+ note: dnR.ok ? 'OK' : 'offline (run: dragon osint serve)',
237
+ })
238
+
239
+ // Wyrm reachable?
240
+ const wyrmR = await ping(`http://localhost:${config.products.wyrm.port ?? 3333}/health`)
241
+ checks.push({
242
+ name: 'Wyrm /health reachable',
243
+ ok: wyrmR.ok,
244
+ note: wyrmR.ok ? 'OK' : 'offline (Wyrm integration becomes best-effort)',
245
+ })
246
+
247
+ checks.forEach((c) => {
248
+ const sym = c.ok ? chalk.green('✓') : chalk.yellow('⚠')
249
+ console.log(` ${sym} ${c.name.padEnd(40)} ${chalk.dim(c.note ?? '')}`)
250
+ })
251
+ console.log()
252
+ const failing = checks.filter((c) => !c.ok).length
253
+ if (failing === 0) success('All contract checks passing')
254
+ else warn(`${failing} check(s) need attention — see ~/Git Projects/dragon-cli/docs/SYSTEM-PREMORTEM.md`)
255
+ })
256
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * dragon keep — DragonKeep defensive engine bridge.
3
+ *
4
+ * DragonKeep ships independently as a Rust binary (`dragonkeep`); this
5
+ * subcommand is a thin shim that surfaces its status + control loop
6
+ * from the unified `dragon` CLI so operators don't need to memorise
7
+ * a second toolchain.
8
+ */
9
+
10
+ import type { Command } from 'commander'
11
+ import type { DragonConfig } from '../config.js'
12
+ import { spawnSync } from 'node:child_process'
13
+ import { error, info, label } from '../utils.js'
14
+ import chalk from 'chalk'
15
+
16
+ function which(bin: string): string | null {
17
+ const r = spawnSync('command', ['-v', bin], { shell: true, encoding: 'utf-8' })
18
+ const out = (r.stdout || '').trim()
19
+ return out || null
20
+ }
21
+
22
+ function passthrough(args: string[]): number {
23
+ const bin = which('dragonkeep')
24
+ if (!bin) {
25
+ error('dragonkeep binary not on PATH')
26
+ info('install it via `dragon install dragonkeep` or build from source in ~/Git Projects/DragonKeep')
27
+ return 127
28
+ }
29
+ const r = spawnSync(bin, args, { stdio: 'inherit' })
30
+ return r.status ?? 1
31
+ }
32
+
33
+ export function registerKeepCommands(program: Command, _config: DragonConfig) {
34
+ const keep = program
35
+ .command('keep')
36
+ .description('DragonKeep — defensive engine (NGAV · MDR · compliance · SOAR)')
37
+
38
+ keep
39
+ .command('status')
40
+ .description('Show DragonKeep daemon status + active engines')
41
+ .action(() => { process.exit(passthrough(['status'])) })
42
+
43
+ keep
44
+ .command('scan [path]')
45
+ .description('Run an on-demand AV scan')
46
+ .action((path?: string) => { process.exit(passthrough(['scan', ...(path ? [path] : [])])) })
47
+
48
+ keep
49
+ .command('engines')
50
+ .description('List the active defensive engines')
51
+ .action(() => { process.exit(passthrough(['engines'])) })
52
+
53
+ // Default action: print a short summary instead of dumping help.
54
+ keep.action(() => {
55
+ console.log()
56
+ console.log(` ${label('DragonKeep')} ${chalk.dim('defensive engine · 28 modules · NGAV + MDR')}`)
57
+ console.log()
58
+ console.log(` ${chalk.dim('dragon keep status')} ${chalk.dim('— daemon + engine health')}`)
59
+ console.log(` ${chalk.dim('dragon keep scan')} ${chalk.dim('— on-demand scan')}`)
60
+ console.log(` ${chalk.dim('dragon keep engines')} ${chalk.dim('— list active engines')}`)
61
+ console.log()
62
+ if (!which('dragonkeep')) {
63
+ info(`run ${chalk.green('dragon install dragonkeep')} to install the daemon`)
64
+ console.log()
65
+ }
66
+ })
67
+ }