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,273 @@
1
+ /**
2
+ * dragon lifecycle commands — bootstrap + update + install + stop
3
+ *
4
+ * dragon up — clone every missing repo, install everything
5
+ * dragon update — git pull every product in the manifest
6
+ * dragon install <key> — install one product
7
+ * dragon uninstall <key> — remove a product directory (asks first)
8
+ * dragon list — print the manifest
9
+ * dragon stop --all — kill every running service (uses ~/.dragon/pids/)
10
+ *
11
+ * Copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
12
+ * Author: Ryan Sebastian <ryan@ghosts.lk>
13
+ */
14
+
15
+ import type { Command } from 'commander'
16
+ import type { DragonConfig } from '../config.js'
17
+ import { loadManifest, resolveRoot, productPath, type ManifestProduct } from '../manifest.js'
18
+ import { run, label, eyebrow, success, error, info, warn, isInstalled, isRunning, readVersion, C } from '../utils.js'
19
+ import chalk from 'chalk'
20
+ import { existsSync, mkdirSync, rmSync } from 'node:fs'
21
+ import { join } from 'node:path'
22
+ import { execSync } from 'node:child_process'
23
+
24
+ function clone(p: ManifestProduct, root: string): boolean {
25
+ const target = join(root, p.dir)
26
+ if (existsSync(join(target, '.git'))) {
27
+ info(`${p.name.padEnd(28)} already cloned`)
28
+ return true
29
+ }
30
+ if (existsSync(target)) {
31
+ warn(`${p.name.padEnd(28)} dir exists but no .git — skipping clone`)
32
+ return true
33
+ }
34
+ info(`${p.name.padEnd(28)} cloning ghosts-lk-style…`)
35
+ try {
36
+ execSync(`git clone https://github.com/${p.repo}.git "${target}"`, { stdio: 'inherit' })
37
+ return true
38
+ } catch (e) {
39
+ error(`${p.name}: clone failed (${e})`)
40
+ return false
41
+ }
42
+ }
43
+
44
+ function pull(p: ManifestProduct, root: string): boolean {
45
+ const target = join(root, p.dir)
46
+ if (!existsSync(join(target, '.git'))) {
47
+ warn(`${p.name.padEnd(28)} not cloned — skipping update`)
48
+ return false
49
+ }
50
+ try {
51
+ execSync(`git -C "${target}" pull --ff-only`, { stdio: 'inherit' })
52
+ return true
53
+ } catch (e) {
54
+ error(`${p.name}: update failed (${e})`)
55
+ return false
56
+ }
57
+ }
58
+
59
+ async function install(p: ManifestProduct, root: string): Promise<boolean> {
60
+ const target = join(root, p.dir)
61
+ if (!existsSync(target)) {
62
+ error(`${p.name}: dir missing at ${target}`)
63
+ return false
64
+ }
65
+ for (const step of p.install || []) {
66
+ info(`${p.name.padEnd(28)} → ${chalk.dim(step)}`)
67
+ const parts = step.split(' ').filter(Boolean)
68
+ const cmd = parts.shift() ?? ''
69
+ const exit = await run(cmd, parts, target)
70
+ if (exit !== 0) {
71
+ error(`${p.name}: install step "${step}" failed (exit ${exit})`)
72
+ return false
73
+ }
74
+ }
75
+ return true
76
+ }
77
+
78
+ function topoSort(products: ManifestProduct[]): ManifestProduct[] {
79
+ // Stable topo sort by `requires` so dependencies install first.
80
+ const out: ManifestProduct[] = []
81
+ const visited = new Set<string>()
82
+ const visiting = new Set<string>()
83
+ const byKey = new Map(products.map((p) => [p.key, p]))
84
+ function dfs(p: ManifestProduct): void {
85
+ if (visited.has(p.key)) return
86
+ if (visiting.has(p.key)) return
87
+ visiting.add(p.key)
88
+ for (const req of p.requires ?? []) {
89
+ const dep = byKey.get(req)
90
+ if (dep) dfs(dep)
91
+ }
92
+ visiting.delete(p.key)
93
+ visited.add(p.key)
94
+ out.push(p)
95
+ }
96
+ for (const p of products) dfs(p)
97
+ return out
98
+ }
99
+
100
+ export function registerLifecycleCommands(program: Command, _config: DragonConfig) {
101
+
102
+ program.command('list')
103
+ .description('Print the dragon stack with live install / running state')
104
+ .option('--stack <stack>', 'Filter by stack (operator|commercial|daemon|library)')
105
+ .option('--verbose', 'Include repo path + version per row')
106
+ .option('--json', 'Machine-readable output (skips pretty print)')
107
+ .action(async (opts) => {
108
+ const m = loadManifest()
109
+ const root = resolveRoot(m)
110
+ const items = opts.stack ? m.product.filter((p) => p.stack === opts.stack) : m.product
111
+
112
+ // Probe install / running / version in parallel so list is snappy.
113
+ type Row = { p: ManifestProduct; installed: boolean; running: boolean; version: string | null }
114
+ const rows: Row[] = await Promise.all(items.map(async (p) => {
115
+ const dir = productPath(m, p)
116
+ const installed = isInstalled(dir)
117
+ const running = installed && p.port ? await isRunning(p.port) : false
118
+ const version = installed ? readVersion(dir) : null
119
+ return { p, installed, running, version }
120
+ }))
121
+
122
+ if (opts.json) {
123
+ console.log(JSON.stringify({
124
+ manifest_version: m.manifest_version, root,
125
+ rows: rows.map((r) => ({
126
+ key: r.p.key, name: r.p.name, stack: r.p.stack, kind: r.p.kind,
127
+ repo: r.p.repo, port: r.p.port,
128
+ installed: r.installed, running: r.running, version: r.version,
129
+ })),
130
+ }, null, 2))
131
+ return
132
+ }
133
+
134
+ console.log()
135
+ console.log(` ${label('Dragon')} ${C.info(`manifest v${m.manifest_version} · root ${root}`)}`)
136
+ console.log()
137
+
138
+ // Stack labels + per-stack accent glyph mirror the rail dividers
139
+ // in the Dragon Console. Glyphs picked to evoke each stack's
140
+ // shape language (chevron / diamond / graph-node / stacked rows).
141
+ const STACK_LABELS: Record<string, string> = {
142
+ operator: 'OPERATOR · internal tools',
143
+ commercial: 'COMMERCIAL · customer-facing',
144
+ daemon: 'DAEMON · background services',
145
+ library: 'LIBRARY · reusable modules',
146
+ }
147
+ const STACK_GLYPH: Record<string, string> = {
148
+ operator: '▲',
149
+ commercial: '◆',
150
+ daemon: '◇',
151
+ library: '▤',
152
+ }
153
+ let section = 0
154
+ for (const stack of ['operator', 'commercial', 'daemon', 'library'] as const) {
155
+ const sub = rows.filter((r) => r.p.stack === stack)
156
+ if (sub.length === 0) continue
157
+ section += 1
158
+ const glyph = C.accent(STACK_GLYPH[stack])
159
+ console.log(` ${glyph} ${eyebrow(section, STACK_LABELS[stack])}`)
160
+ for (const { p, installed, running, version } of sub) {
161
+ // Three-state mark — matches the web UI status dots:
162
+ // ● hot-teal (running) · ✓ teal (installed) · ○ grey (available)
163
+ const mark =
164
+ running ? C.hot.bold('●') :
165
+ installed ? C.accent('✓') :
166
+ C.info('○')
167
+ const nameRaw = p.name.padEnd(28)
168
+ const name = installed ? chalk.bold(nameRaw) : C.info(nameRaw)
169
+ const port = p.port ? C.info(`:${p.port}`.padStart(6)) : ' '
170
+ const ver = version ? C.faint(` v${version}`.padEnd(11)) : C.faint(' —'.padEnd(11))
171
+ const repo = opts.verbose ? ` ${C.faint(p.repo)}` : ''
172
+ console.log(` ${mark} ${name}${port}${ver}${repo}`)
173
+ }
174
+ console.log()
175
+ }
176
+
177
+ const installed = rows.filter((r) => r.installed).length
178
+ const running = rows.filter((r) => r.running).length
179
+ console.log(` ${C.faint('—')}`)
180
+ console.log(
181
+ ` ${C.info(`${rows.length} total`)}` +
182
+ ` ${C.accent(`✓ ${installed} installed`)}` +
183
+ ` ${C.hot.bold(`● ${running} running`)}` +
184
+ ` ${C.info(`○ ${rows.length - installed} available`)}`,
185
+ )
186
+ console.log()
187
+ console.log(` ${C.faint('next:')} ${C.accent('dragon install <key>')} · ${C.accent('dragon up')} (everything) · ${C.accent('dragon serve')} (start daemons)`)
188
+ console.log()
189
+ })
190
+
191
+ program.command('up')
192
+ .description('Clone every missing repo + install — full fresh-machine bootstrap')
193
+ .option('--stack <stack>', 'Limit to one stack')
194
+ .option('--dry-run', 'Show what would happen, take no action')
195
+ .action(async (opts) => {
196
+ const m = loadManifest()
197
+ const root = resolveRoot(m)
198
+ mkdirSync(root, { recursive: true })
199
+ const items = topoSort(opts.stack ? m.product.filter((p) => p.stack === opts.stack) : m.product)
200
+ console.log(label('Dragon'), `Bootstrapping ${items.length} product(s) into ${root}\n`)
201
+ let ok = 0
202
+ for (const p of items) {
203
+ if (opts.dryRun) {
204
+ info(`would clone + install ${p.name}`)
205
+ continue
206
+ }
207
+ if (!clone(p, root)) continue
208
+ if (await install(p, root)) {
209
+ ok++
210
+ success(`${p.name} ready`)
211
+ }
212
+ }
213
+ console.log()
214
+ success(`${ok} of ${items.length} ready · run \`dragon serve\` to start daemons`)
215
+ })
216
+
217
+ program.command('update')
218
+ .description('git pull every product in the manifest')
219
+ .option('--stack <stack>', 'Limit to one stack')
220
+ .action((opts) => {
221
+ const m = loadManifest()
222
+ const root = resolveRoot(m)
223
+ const items = opts.stack ? m.product.filter((p) => p.stack === opts.stack) : m.product
224
+ console.log(label('Dragon'), `Pulling latest for ${items.length} product(s)\n`)
225
+ let ok = 0
226
+ for (const p of items) {
227
+ if (pull(p, root)) ok++
228
+ }
229
+ console.log()
230
+ success(`${ok} of ${items.length} updated`)
231
+ })
232
+
233
+ program.command('install <key>')
234
+ .description('Install one product (clone + install) from the manifest')
235
+ .action(async (key: string) => {
236
+ const m = loadManifest()
237
+ const root = resolveRoot(m)
238
+ const p = m.product.find((q) => q.key === key)
239
+ if (!p) {
240
+ error(`Unknown product key '${key}'`)
241
+ info(`Run \`dragon list\` to see all keys.`)
242
+ process.exit(2)
243
+ }
244
+ // Install requires first
245
+ const order = topoSort([p])
246
+ for (const dep of order) {
247
+ if (!existsSync(productPath(m, dep))) clone(dep, root)
248
+ if (!await install(dep, root)) {
249
+ error(`${dep.name} install failed`)
250
+ process.exit(3)
251
+ }
252
+ }
253
+ success(`${p.name} ready`)
254
+ })
255
+
256
+ program.command('uninstall <key>')
257
+ .description('Remove a product directory (asks first)')
258
+ .option('-y, --yes', 'Skip confirmation prompt')
259
+ .action(async (key: string, opts) => {
260
+ const m = loadManifest()
261
+ const p = m.product.find((q) => q.key === key)
262
+ if (!p) { error(`Unknown product '${key}'`); process.exit(2) }
263
+ const target = productPath(m, p)
264
+ if (!existsSync(target)) { info(`Not installed at ${target}`); return }
265
+ if (!opts.yes) {
266
+ warn(`This will rm -rf ${target}`)
267
+ info(`Re-run with -y to confirm.`)
268
+ return
269
+ }
270
+ rmSync(target, { recursive: true, force: true })
271
+ success(`Removed ${target}`)
272
+ })
273
+ }
@@ -0,0 +1,184 @@
1
+ /**
2
+ * dragon login / logout / whoami — authenticate the CLI with the Dragon assistant
3
+ * (account.ghosts.lk).
4
+ *
5
+ * Default is a browser DEVICE-CODE flow (like gh/Claude Code): `dragon login` opens
6
+ * the approve page, you confirm while signed in, and the CLI receives a 90-day bearer
7
+ * token — no cookie pasting. Fallbacks: `--session <gp_session>` (works even before the
8
+ * device-flow backend is deployed) and `--token <pat>`. Credentials live in
9
+ * ~/.dragon/config.json (locked to 0600).
10
+ *
11
+ * Copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
12
+ */
13
+
14
+ import type { Command } from 'commander'
15
+ import type { DragonConfig } from '../config.js'
16
+ import { resolveAuth, saveAuth, clearAuth, whoami, isBrowsableHttpUrl } from '../auth.js'
17
+ import { C, success, error, info } from '../utils.js'
18
+ import { createInterface } from 'node:readline/promises'
19
+ import { stdin, stdout } from 'node:process'
20
+ import { spawn } from 'node:child_process'
21
+ import { chmodSync } from 'node:fs'
22
+ import { homedir } from 'node:os'
23
+ import { join } from 'node:path'
24
+
25
+ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
26
+
27
+ /** Open a URL in the browser. Returns false (without spawning) if the URL isn't
28
+ * something a browser will accept — so we never trigger "this address is
29
+ * restricted"; the caller has already printed the URL for manual use. */
30
+ function openBrowser(url: string): boolean {
31
+ if (!isBrowsableHttpUrl(url)) return false
32
+ const isMac = process.platform === 'darwin'
33
+ const isWin = process.platform === 'win32'
34
+ const cmd = isMac ? 'open' : isWin ? 'cmd' : 'xdg-open'
35
+ const args = isWin ? ['/c', 'start', '', url] : [url]
36
+ try {
37
+ const child = spawn(cmd, args, { stdio: 'ignore', detached: true })
38
+ child.on('error', () => {})
39
+ child.unref()
40
+ return true
41
+ } catch {
42
+ return false
43
+ }
44
+ }
45
+
46
+ function lockConfig(): void {
47
+ try { chmodSync(join(homedir(), '.dragon', 'config.json'), 0o600) } catch { /* best-effort */ }
48
+ }
49
+
50
+ async function verifyAndReport(): Promise<boolean> {
51
+ const me = await whoami()
52
+ if (me.ok) {
53
+ if (me.email) saveAuth({ email: me.email })
54
+ lockConfig()
55
+ success(`signed in${me.email ? ` as ${C.accent(me.email)}` : ''}`)
56
+ return true
57
+ }
58
+ if (me.status === 401) error('credential rejected — sign in again.')
59
+ else error(`could not verify (${me.error ?? me.status}).`)
60
+ return false
61
+ }
62
+
63
+ interface StartResp { device_code: string; user_code: string; verification_uri: string; verification_uri_complete: string; interval: number; expires_in: number }
64
+
65
+ /** Returns 'ok' | 'failed' | 'unavailable' (unavailable → caller falls back to paste). */
66
+ async function deviceFlow(apiBase: string, open: boolean): Promise<'ok' | 'failed' | 'unavailable'> {
67
+ let start: StartResp
68
+ try {
69
+ const r = await fetch(`${apiBase}/api/v1/cli/auth/start`, { method: 'POST' })
70
+ if (r.status === 404) return 'unavailable' // route not deployed → caller offers paste
71
+ if (!r.ok) { error(`login couldn't start: HTTP ${r.status} from ${apiBase}.`); return 'failed' }
72
+ start = (await r.json()) as StartResp
73
+ if (!start?.verification_uri_complete || !start?.device_code) { error('the server returned an unexpected response.'); return 'failed' }
74
+ } catch (e) {
75
+ error(`couldn't reach ${apiBase} — ${e instanceof Error ? e.message : String(e)}.`)
76
+ info('check your connection, or pass a valid endpoint with `--api`.')
77
+ return 'failed'
78
+ }
79
+
80
+ console.log()
81
+ console.log(` ${C.accent.bold('Dragon login')} ${C.faint('· ' + apiBase)}`)
82
+ console.log(` Approve this device in your browser:`)
83
+ console.log(` ${C.info(start.verification_uri_complete)}`)
84
+ console.log(` Verification code: ${C.accent.bold(start.user_code)}`)
85
+ console.log()
86
+ if (open && !openBrowser(start.verification_uri_complete)) info('open the link above manually to approve.')
87
+
88
+ const deadline = Date.now() + start.expires_in * 1000
89
+ const interval = Math.max(2, start.interval || 3) * 1000
90
+ process.stdout.write(` ${C.faint('waiting for approval')} `)
91
+ while (Date.now() < deadline) {
92
+ await sleep(interval)
93
+ process.stdout.write(C.faint('·'))
94
+ let poll: { status: string; token?: string }
95
+ try {
96
+ poll = (await fetch(`${apiBase}/api/v1/cli/auth/poll`, {
97
+ method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ device_code: start.device_code }),
98
+ }).then((r) => r.json())) as { status: string; token?: string }
99
+ } catch {
100
+ continue
101
+ }
102
+ if (poll.status === 'approved' && poll.token) {
103
+ process.stdout.write('\n')
104
+ saveAuth({ token: poll.token, session: undefined }) // token wins; drop any stale session
105
+ return (await verifyAndReport()) ? 'ok' : 'failed'
106
+ }
107
+ if (poll.status === 'denied') { process.stdout.write('\n'); error('approval was denied in the browser.'); return 'failed' }
108
+ if (poll.status === 'expired' || poll.status === 'not_found') { process.stdout.write('\n'); error('the code expired — run `dragon login` again.'); return 'failed' }
109
+ }
110
+ process.stdout.write('\n'); error('timed out waiting for approval.')
111
+ return 'failed'
112
+ }
113
+
114
+ async function pasteFlow(apiBase: string, open: boolean): Promise<boolean> {
115
+ console.log()
116
+ console.log(` ${C.accent.bold('Dragon login')} ${C.faint('· ' + apiBase)} ${C.faint('(session paste)')}`)
117
+ console.log(` ${C.faint('1.')} Sign in at ${C.info(apiBase + '/login')}`)
118
+ console.log(` ${C.faint('2.')} DevTools → Application → Cookies → ${C.info(new URL(apiBase).host)}`)
119
+ console.log(` ${C.faint('3.')} Copy the ${C.accent('gp_session')} value and paste it below.`)
120
+ console.log()
121
+ if (open && !openBrowser(`${apiBase}/login`)) info('open the sign-in link above manually.')
122
+ const rl = createInterface({ input: stdin, output: stdout })
123
+ let value = ''
124
+ try { value = (await rl.question(` ${C.accent('gp_session')} ▸ `)).trim() } finally { rl.close() }
125
+ if (!value) { error('no value entered — aborted.'); return false }
126
+ saveAuth({ session: value, token: undefined })
127
+ return verifyAndReport()
128
+ }
129
+
130
+ export function registerLoginCommands(program: Command, _config: DragonConfig) {
131
+ program
132
+ .command('login')
133
+ .description('Authenticate the CLI (browser device-code flow by default)')
134
+ .option('--token <token>', 'use a personal-access-token directly')
135
+ .option('--session <value>', 'use a gp_session cookie value (fallback)')
136
+ .option('--paste', 'force the gp_session paste flow instead of the browser device flow')
137
+ .option('--api <url>', 'point at a non-default origin (e.g. http://localhost:8799)')
138
+ .option('--no-open', "don't auto-open the browser")
139
+ .action(async (opts: { token?: string; session?: string; paste?: boolean; api?: string; open?: boolean }) => {
140
+ try {
141
+ if (opts.api) {
142
+ const base = opts.api.replace(/\/+$/, '')
143
+ if (!isBrowsableHttpUrl(base)) { error(`--api "${opts.api}" isn't a usable http(s) URL (or it uses a restricted port).`); process.exitCode = 1; return }
144
+ saveAuth({ apiBase: base })
145
+ }
146
+ const { apiBase } = resolveAuth() // sanitized — a bad stored value self-heals to the default
147
+
148
+ if (opts.token) { saveAuth({ token: opts.token, session: undefined }); if (!(await verifyAndReport())) process.exitCode = 1; return }
149
+ if (opts.session) { saveAuth({ session: opts.session.trim(), token: undefined }); if (!(await verifyAndReport())) process.exitCode = 1; return }
150
+ if (opts.paste) { if (!(await pasteFlow(apiBase, opts.open !== false))) process.exitCode = 1; return }
151
+
152
+ const result = await deviceFlow(apiBase, opts.open !== false)
153
+ if (result === 'unavailable') {
154
+ info('device-code flow not available on this server yet — falling back to session paste.')
155
+ if (!(await pasteFlow(apiBase, opts.open !== false))) process.exitCode = 1
156
+ return
157
+ }
158
+ if (result === 'ok') info('run `dragon chat` to start, or `dragon ask "…"` for one-shot.')
159
+ else process.exitCode = 1
160
+ } catch (e) {
161
+ error(`login failed: ${e instanceof Error ? e.message : String(e)}`)
162
+ process.exitCode = 1
163
+ }
164
+ })
165
+
166
+ program
167
+ .command('logout')
168
+ .description('Forget the stored Dragon assistant credentials')
169
+ .action(() => { clearAuth(); success('signed out — credentials cleared from ~/.dragon/config.json') })
170
+
171
+ program
172
+ .command('whoami')
173
+ .description('Show the current Dragon assistant identity + endpoint')
174
+ .action(async () => {
175
+ const { apiBase, mode, email } = resolveAuth()
176
+ console.log(` ${C.faint('endpoint:')} ${C.info(apiBase)}`)
177
+ console.log(` ${C.faint('auth:')} ${mode === 'none' ? C.faint('none') : C.accent(mode)}`)
178
+ if (mode === 'none') { info('not signed in — run `dragon login`.'); return }
179
+ const me = await whoami()
180
+ if (me.ok) success(`verified${me.email ? ` as ${C.accent(me.email)}` : ''}`)
181
+ else if (me.status === 401) error('stored credential is no longer valid — run `dragon login`.')
182
+ else { error(`could not verify (${me.error ?? me.status}).`); if (email) info(`last known: ${email}`) }
183
+ })
184
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * dragon completions / upgrade — quality-of-life maintenance.
3
+ *
4
+ * dragon completions [bash|zsh|fish] # shell tab-completion script
5
+ * dragon upgrade # git pull + build (from-source installs)
6
+ *
7
+ * Copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
8
+ */
9
+
10
+ import type { Command } from 'commander'
11
+ import type { DragonConfig } from '../config.js'
12
+ import { C, success, error, info } from '../utils.js'
13
+ import { execFile } from 'node:child_process'
14
+ import { existsSync, realpathSync } from 'node:fs'
15
+ import { dirname, join } from 'node:path'
16
+
17
+ function installRoot(): string | null {
18
+ const entry = process.argv[1]
19
+ if (!entry) return null
20
+ try { return dirname(dirname(realpathSync(entry))) } catch { return dirname(dirname(entry)) } // dist/index.js → root
21
+ }
22
+
23
+ function run(cmd: string, args: string[], cwd: string): Promise<number> {
24
+ return new Promise((res) => {
25
+ execFile(cmd, args, { cwd }, (err, stdout, stderr) => { process.stdout.write((stdout || '') + (stderr || '')); res(err ? 1 : 0) })
26
+ })
27
+ }
28
+
29
+ export function registerMaintenanceCommands(program: Command, _config: DragonConfig) {
30
+ program
31
+ .command('completions [shell]')
32
+ .description('Print a shell completion script (bash | zsh | fish)')
33
+ .action((shell = 'bash') => {
34
+ const cmds = program.commands.map((c) => c.name()).filter((n) => n && n !== 'completions').sort().join(' ')
35
+ if (shell === 'fish') console.log(`complete -c dragon -f -a "${cmds}"`)
36
+ else if (shell === 'zsh') console.log(`#compdef dragon\n_dragon(){ _arguments '1:command:(${cmds})' '*::arg:_files' }\ncompdef _dragon dragon`)
37
+ else console.log(`_dragon(){ COMPREPLY=( $(compgen -W "${cmds}" -- "\${COMP_WORDS[1]}") ); }\ncomplete -F _dragon dragon`)
38
+ process.stderr.write(C.faint(`# install: dragon completions ${shell} >> ~/.${shell === 'fish' ? 'config/fish/completions/dragon.fish' : shell + 'rc'}\n`))
39
+ })
40
+
41
+ program
42
+ .command('upgrade')
43
+ .description('Update the dragon CLI from source (git pull + build)')
44
+ .action(async () => {
45
+ const root = installRoot()
46
+ if (!root) { error('could not locate the install directory'); process.exitCode = 1; return }
47
+ if (!existsSync(join(root, '.git'))) { info(`installed at ${C.info(root)} — not a git checkout, update it however you installed it.`); return }
48
+ info(`upgrading dragon at ${C.info(root)} …`)
49
+ if (await run('git', ['pull', '--ff-only'], root)) { error('git pull failed (commit/stash local changes first?)'); process.exitCode = 1; return }
50
+ if (await run('npm', ['install'], root)) { error('npm install failed'); process.exitCode = 1; return }
51
+ if (await run('npm', ['run', 'build'], root)) { error('build failed'); process.exitCode = 1; return }
52
+ success('dragon upgraded.')
53
+ })
54
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * dragon mcp — manage extra MCP servers wired into the agent.
3
+ *
4
+ * dragon mcp add <name> <command> [args...] # e.g. dragon mcp add fs npx -- -y @modelcontextprotocol/server-filesystem ~/code
5
+ * dragon mcp list
6
+ * dragon mcp remove <name>
7
+ *
8
+ * Configured servers connect on `dragon chat` and their tools become agent tools
9
+ * (namespaced <server>__<tool>). Wyrm is built-in and not managed here.
10
+ *
11
+ * Copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
12
+ */
13
+
14
+ import type { Command } from 'commander'
15
+ import { loadConfig, saveConfig, type DragonConfig } from '../config.js'
16
+ import { C, success, error, info } from '../utils.js'
17
+ import { panel, chrome, statusDot } from '../ui.js'
18
+
19
+ export function registerMcpCommands(program: Command, _config: DragonConfig) {
20
+ const mcp = program.command('mcp').description('Manage MCP servers wired into the agent')
21
+
22
+ mcp
23
+ .command('add <name> <command> [args...]')
24
+ .description('Add an MCP server (stdio). Args after the command are passed to it.')
25
+ .action((name: string, command: string, args: string[] = []) => {
26
+ const c = loadConfig()
27
+ c.mcpServers = { ...(c.mcpServers ?? {}), [name]: { command, args } }
28
+ saveConfig(c)
29
+ success(`mcp server "${name}" added → ${command} ${args.join(' ')}`)
30
+ info('it connects on `dragon chat`; its tools appear namespaced as ' + C.info(`${name}__<tool>`))
31
+ })
32
+
33
+ mcp
34
+ .command('remove <name>')
35
+ .description('Remove a configured MCP server')
36
+ .action((name: string) => {
37
+ const c = loadConfig()
38
+ if (!c.mcpServers?.[name]) { error(`no MCP server named "${name}"`); process.exitCode = 1; return }
39
+ delete c.mcpServers[name]
40
+ saveConfig(c)
41
+ success(`removed "${name}"`)
42
+ })
43
+
44
+ mcp
45
+ .command('list')
46
+ .description('List configured MCP servers')
47
+ .action(() => {
48
+ const servers = loadConfig().mcpServers ?? {}
49
+ const names = Object.keys(servers)
50
+ const lines = names.length
51
+ ? names.map((n) => `${statusDot(true)} ${C.info(n.padEnd(12))} ${C.faint(servers[n].command + ' ' + (servers[n].args ?? []).join(' '))}`)
52
+ : [C.faint('none — add one with `dragon mcp add <name> <command> [args...]`')]
53
+ lines.push('', C.faint('Wyrm memory is built-in (not listed here).'))
54
+ console.log()
55
+ console.log(panel(lines, { title: chrome('MCP SERVERS') }))
56
+ })
57
+ }