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,185 @@
1
+ /**
2
+ * dragon scale — DragonScale Commerce Platform management
3
+ *
4
+ * Manages Upalis/DragonScale deployments:
5
+ * - Status & health checks via API
6
+ * - Order management
7
+ * - Menu management
8
+ * - Database operations (backup, seed)
9
+ * - Deployment (pull, update)
10
+ */
11
+
12
+ import type { Command } from 'commander'
13
+ import type { DragonConfig } from '../config.js'
14
+ import { getProductPath } from '../config.js'
15
+ import { exec, run, fetchJSON, label, success, error, info, warn, table } from '../utils.js'
16
+ import chalk from 'chalk'
17
+ import { existsSync } from 'fs'
18
+ import { join } from 'path'
19
+
20
+ export function registerScaleCommands(program: Command, config: DragonConfig) {
21
+ const scale = program
22
+ .command('scale')
23
+ .description('DragonScale — Commerce platform management')
24
+
25
+ // --- status ---
26
+ scale
27
+ .command('status')
28
+ .description('Check platform health & today\'s stats')
29
+ .option('-u, --url <url>', 'Base URL of the DragonScale instance')
30
+ .action(async (opts) => {
31
+ const url = opts.url || config.products.scale.url
32
+ if (!url) {
33
+ error('No URL configured. Use --url or run: dragon init')
34
+ process.exit(1)
35
+ }
36
+ console.log(label('DragonScale'), 'Checking status...\n')
37
+ try {
38
+ const health = await fetchJSON(`${url}/api/health.php`)
39
+ console.log(` Platform: ${health.status === 'healthy' ? chalk.green('● Online') : chalk.red('● Down')}`)
40
+ console.log(` PHP: ${chalk.dim(health.checks?.php?.version || 'unknown')}`)
41
+ console.log(` Database: ${health.checks?.database?.status === 'connected' ? chalk.green('Connected') : chalk.red('Disconnected')}`)
42
+ console.log(` Disk: ${chalk.dim(health.checks?.filesystem?.status || 'unknown')}`)
43
+ success('Health check passed')
44
+ } catch (e: any) {
45
+ error(`Cannot reach ${url}/api/health.php — ${e.message}`)
46
+ }
47
+ })
48
+
49
+ // --- orders ---
50
+ scale
51
+ .command('orders')
52
+ .description('List recent orders')
53
+ .option('-u, --url <url>', 'Base URL')
54
+ .option('-n, --limit <n>', 'Number of orders', '10')
55
+ .option('-s, --status <status>', 'Filter by status (pending, confirmed, preparing, ready, delivered)')
56
+ .action(async (opts) => {
57
+ const url = opts.url || config.products.scale.url
58
+ if (!url) { error('No URL configured.'); process.exit(1) }
59
+ console.log(label('DragonScale'), 'Fetching orders...\n')
60
+ try {
61
+ const params = new URLSearchParams({ action: 'list', limit: opts.limit })
62
+ if (opts.status) params.set('status', opts.status)
63
+ const data = await fetchJSON(`${url}/api/orders.php?${params}`)
64
+ if (data.orders?.length) {
65
+ table(data.orders.map((o: any) => ({
66
+ ID: `#${o.id}`,
67
+ Status: o.status,
68
+ Type: o.order_type || '-',
69
+ Total: `LKR ${o.total}`,
70
+ Time: o.created_at || '-',
71
+ })))
72
+ } else {
73
+ info('No orders found')
74
+ }
75
+ } catch (e: any) {
76
+ error(`Failed to fetch orders: ${e.message}`)
77
+ }
78
+ })
79
+
80
+ // --- menu ---
81
+ scale
82
+ .command('menu')
83
+ .description('List menu items')
84
+ .option('-u, --url <url>', 'Base URL')
85
+ .option('-c, --category <id>', 'Filter by category ID')
86
+ .action(async (opts) => {
87
+ const url = opts.url || config.products.scale.url
88
+ if (!url) { error('No URL configured.'); process.exit(1) }
89
+ console.log(label('DragonScale'), 'Fetching menu...\n')
90
+ try {
91
+ const params = new URLSearchParams({ action: 'all' })
92
+ if (opts.category) params.set('category_id', opts.category)
93
+ const data = await fetchJSON(`${url}/api/menu.php?${params}`)
94
+ if (data.categories?.length) {
95
+ for (const cat of data.categories) {
96
+ console.log(chalk.green.bold(`\n ${cat.name}`))
97
+ if (cat.items?.length) {
98
+ for (const item of cat.items) {
99
+ const status = item.available ? chalk.green('●') : chalk.red('○')
100
+ console.log(` ${status} ${item.name} ${chalk.dim(`LKR ${item.price}`)}`)
101
+ }
102
+ }
103
+ }
104
+ console.log()
105
+ } else {
106
+ info('No menu data')
107
+ }
108
+ } catch (e: any) {
109
+ error(`Failed to fetch menu: ${e.message}`)
110
+ }
111
+ })
112
+
113
+ // --- deploy ---
114
+ scale
115
+ .command('deploy')
116
+ .description('Pull latest code & update production')
117
+ .action(async () => {
118
+ const path = getProductPath(config, 'scale')
119
+ const script = join(path, 'scripts/update-production.sh')
120
+ if (!existsSync(script)) {
121
+ error(`Deploy script not found: ${script}`)
122
+ process.exit(1)
123
+ }
124
+ console.log(label('DragonScale'), 'Deploying...\n')
125
+ const code = await run('bash', [script], path)
126
+ code === 0 ? success('Deployment complete') : error(`Deploy failed (exit ${code})`)
127
+ })
128
+
129
+ // --- backup ---
130
+ scale
131
+ .command('backup')
132
+ .description('Backup the database')
133
+ .option('-o, --output <dir>', 'Output directory', './backups')
134
+ .action(async (opts) => {
135
+ const path = getProductPath(config, 'scale')
136
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)
137
+ const outDir = join(path, opts.output)
138
+ console.log(label('DragonScale'), 'Backing up database...\n')
139
+ try {
140
+ exec(`mkdir -p "${outDir}"`)
141
+ // Try mysqldump first, fall back to sqlite
142
+ try {
143
+ exec(`mysqldump --defaults-file="${join(path, '.my.cnf')}" upalis > "${join(outDir, `backup-${timestamp}.sql`)}"`, path)
144
+ } catch {
145
+ const dbFile = join(path, 'data', 'database.sqlite')
146
+ if (existsSync(dbFile)) {
147
+ exec(`cp "${dbFile}" "${join(outDir, `backup-${timestamp}.sqlite`)}"`, path)
148
+ } else {
149
+ throw new Error('No database found to backup')
150
+ }
151
+ }
152
+ success(`Backup saved to ${outDir}/backup-${timestamp}.*`)
153
+ } catch (e: any) {
154
+ error(`Backup failed: ${e.message}`)
155
+ }
156
+ })
157
+
158
+ // --- seed ---
159
+ scale
160
+ .command('seed')
161
+ .description('Seed database with sample data')
162
+ .action(async () => {
163
+ const path = getProductPath(config, 'scale')
164
+ console.log(label('DragonScale'), 'Seeding database...\n')
165
+ const code = await run('php', ['scripts/seed-production.php'], path)
166
+ code === 0 ? success('Seeding complete') : error(`Seed failed (exit ${code})`)
167
+ })
168
+
169
+ // --- logs ---
170
+ scale
171
+ .command('logs')
172
+ .description('Tail application logs')
173
+ .option('-n, --lines <n>', 'Number of lines', '50')
174
+ .option('-f, --follow', 'Follow log output')
175
+ .action(async (opts) => {
176
+ const path = getProductPath(config, 'scale')
177
+ const logFile = join(path, 'logs/app.log')
178
+ if (!existsSync(logFile)) {
179
+ warn(`No log file at ${logFile}`)
180
+ return
181
+ }
182
+ const args = [opts.follow ? '-f' : `-n${opts.lines}`, logFile]
183
+ await run('tail', args, path)
184
+ })
185
+ }
@@ -0,0 +1,171 @@
1
+ /**
2
+ * dragon serve — Start the entire stack
3
+ *
4
+ * Spawns:
5
+ * - PhantomDragon Control API (Python uvicorn :4091)
6
+ * - PhantomDragon Control dashboard (Next.js :4090)
7
+ * - DragonNet API (Fastify :4080)
8
+ * - DragonNet dashboard (Next.js :4081)
9
+ *
10
+ * PID files at ~/.dragon/pids/ so `dragon down` can stop them cleanly.
11
+ * Foreground mode tails stdout; --detach forks to background.
12
+ */
13
+
14
+ import type { Command } from 'commander'
15
+ import type { DragonConfig } from '../config.js'
16
+ import { label, success, error, info, warn } from '../utils.js'
17
+ import chalk from 'chalk'
18
+ import { spawn } from 'child_process'
19
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, unlinkSync } from 'fs'
20
+ import { homedir } from 'os'
21
+ import { join } from 'path'
22
+
23
+ const PIDS_DIR = join(homedir(), '.dragon', 'pids')
24
+ const LOGS_DIR = join(homedir(), '.dragon', 'logs')
25
+
26
+ function ensureDirs() {
27
+ mkdirSync(PIDS_DIR, { recursive: true })
28
+ mkdirSync(LOGS_DIR, { recursive: true })
29
+ }
30
+
31
+ function pidAlive(pid: number): boolean {
32
+ try { process.kill(pid, 0); return true } catch { return false }
33
+ }
34
+
35
+ interface Service {
36
+ key: string
37
+ name: string
38
+ cwd: string
39
+ cmd: string
40
+ args: string[]
41
+ env?: Record<string, string>
42
+ port?: number
43
+ }
44
+
45
+ function buildServices(config: DragonConfig): Service[] {
46
+ const services: Service[] = []
47
+ const pentest = config.products.pentest.path
48
+ if (pentest && existsSync(join(pentest, 'phantomdragon.py'))) {
49
+ services.push({
50
+ key: 'pd-control-api',
51
+ name: 'PhantomDragon Control API',
52
+ cwd: pentest,
53
+ cmd: 'python3',
54
+ args: ['-m', 'phantom_dragon_ai.control_api'],
55
+ port: config.products.pentest.controlPort,
56
+ })
57
+ }
58
+ // The Next.js dashboard lives in a sibling dir
59
+ const controlDash = pentest ? join(pentest, '..', 'phantomdragon-control') : ''
60
+ if (controlDash && existsSync(controlDash) && existsSync(join(controlDash, 'package.json'))) {
61
+ services.push({
62
+ key: 'pd-control-ui',
63
+ name: 'PhantomDragon Control UI',
64
+ cwd: controlDash,
65
+ cmd: 'npm',
66
+ args: ['run', 'dev'],
67
+ port: config.products.pentest.controlUiPort,
68
+ })
69
+ }
70
+ const net = config.products.net.path
71
+ if (net && existsSync(join(net, 'pnpm-workspace.yaml'))) {
72
+ services.push({
73
+ key: 'dn-api',
74
+ name: 'DragonNet API',
75
+ cwd: net,
76
+ cmd: 'pnpm',
77
+ args: ['--filter', '@dragonnet/api', 'dev'],
78
+ port: config.products.net.apiPort,
79
+ })
80
+ services.push({
81
+ key: 'dn-ui',
82
+ name: 'DragonNet Dashboard',
83
+ cwd: net,
84
+ cmd: 'pnpm',
85
+ args: ['--filter', '@dragonnet/dashboard', 'dev'],
86
+ port: config.products.net.uiPort,
87
+ })
88
+ }
89
+ return services
90
+ }
91
+
92
+ function startService(s: Service): number {
93
+ const logPath = join(LOGS_DIR, `${s.key}.log`)
94
+ const child = spawn(s.cmd, s.args, {
95
+ cwd: s.cwd,
96
+ detached: true,
97
+ stdio: ['ignore', 'pipe', 'pipe'],
98
+ env: { ...process.env, ...(s.env ?? {}) },
99
+ })
100
+ const { openSync, closeSync } = require('fs') as typeof import('fs')
101
+ const fd = openSync(logPath, 'a')
102
+ child.stdout?.on('data', (chunk: Buffer) => writeFileSync(logPath, chunk, { flag: 'a' }))
103
+ child.stderr?.on('data', (chunk: Buffer) => writeFileSync(logPath, chunk, { flag: 'a' }))
104
+ child.unref()
105
+ closeSync(fd)
106
+ writeFileSync(join(PIDS_DIR, `${s.key}.pid`), String(child.pid))
107
+ return child.pid ?? 0
108
+ }
109
+
110
+ export function registerServeCommands(program: Command, config: DragonConfig) {
111
+ program
112
+ .command('serve')
113
+ .description('Start the entire dragon stack (PhantomDragon Control + DragonNet)')
114
+ .option('--detach', 'Background mode (default: foreground)', false)
115
+ .action(async () => {
116
+ ensureDirs()
117
+ const services = buildServices(config)
118
+ if (services.length === 0) {
119
+ error('No products configured. Run: dragon init')
120
+ return
121
+ }
122
+ console.log(label('Dragon'), 'Starting stack...\n')
123
+ for (const s of services) {
124
+ const pidFile = join(PIDS_DIR, `${s.key}.pid`)
125
+ if (existsSync(pidFile)) {
126
+ const oldPid = Number(readFileSync(pidFile, 'utf-8'))
127
+ if (pidAlive(oldPid)) {
128
+ info(`${s.name} already running (pid ${oldPid})`)
129
+ continue
130
+ }
131
+ }
132
+ try {
133
+ const pid = startService(s)
134
+ success(`${s.name.padEnd(28)} ${chalk.dim(`pid ${pid}` + (s.port ? ` :${s.port}` : ''))}`)
135
+ } catch (e) {
136
+ error(`${s.name} → ${e}`)
137
+ }
138
+ }
139
+ console.log()
140
+ info('Logs at ~/.dragon/logs/<service>.log')
141
+ info('Stop with: dragon down')
142
+ })
143
+
144
+ program
145
+ .command('down')
146
+ .description('Stop all dragon services')
147
+ .action(() => {
148
+ ensureDirs()
149
+ const files = readdirSync(PIDS_DIR).filter((f) => f.endsWith('.pid'))
150
+ if (files.length === 0) {
151
+ info('Nothing to stop.')
152
+ return
153
+ }
154
+ for (const f of files) {
155
+ const pidPath = join(PIDS_DIR, f)
156
+ const pid = Number(readFileSync(pidPath, 'utf-8'))
157
+ const name = f.replace(/\.pid$/, '')
158
+ if (pidAlive(pid)) {
159
+ try {
160
+ process.kill(-pid, 'SIGTERM')
161
+ } catch {
162
+ try { process.kill(pid, 'SIGTERM') } catch { /* swallow */ }
163
+ }
164
+ success(`Stopped ${name} (pid ${pid})`)
165
+ } else {
166
+ warn(`${name} not running`)
167
+ }
168
+ try { unlinkSync(pidPath) } catch { /* swallow */ }
169
+ }
170
+ })
171
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * dragon tui — the operator command-center. Full-screen, in the ghosts.lk visual
3
+ * language: near-black depth, a brushed-chrome phantom sigil + wordmark, two-column
4
+ * live panels (system · signal) with a traces-per-hour sparkline + disk gauge, and a
5
+ * pulsing emerald OPSEC bar.
6
+ *
7
+ * Rendered like a flagship TUI (see the advanced-tui-design + terminal-subcell-graphics
8
+ * skills): runs on the ALTERNATE screen buffer (htop-style, restores your scrollback),
9
+ * hides the cursor, and paints each frame inside SYNCHRONIZED OUTPUT (DEC ?2026) with
10
+ * per-line clear-to-EOL — so frames are atomic and flicker-free. `q` / Ctrl-C to quit.
11
+ *
12
+ * Copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
13
+ */
14
+
15
+ import type { Command } from 'commander'
16
+ import { loadConfig, type DragonConfig } from '../config.js'
17
+ import { C } from '../utils.js'
18
+ import { panel, chrome, chromeRule, joinColumns, emerald, brailleChart, gauge } from '../ui.js'
19
+ import { resolveAuth } from '../auth.js'
20
+ import { resolveProvider } from '../brain/index.js'
21
+ import { loadSkillLibrary } from '../agent/skills.js'
22
+ import { existsSync, statfsSync, readFileSync } from 'node:fs'
23
+ import { homedir } from 'node:os'
24
+ import { join } from 'node:path'
25
+ import { stdin, stdout } from 'node:process'
26
+
27
+ const SIGIL = [' ▄▄▄▄▄ ', ' ▟█████▙ ', ' █ ▘ ▘ █ ', ' ▜█████▛ ', ' ▘▘ ▘▘ ']
28
+ const SIGIL_W = 11
29
+ const VER = '4.2.1'
30
+ const PULSE = ['·', '•', '●', '•'] // heartbeat
31
+ const dot = (on: boolean) => (on ? emerald('●') : C.faint('○'))
32
+ const indent = (s: string) => s.split('\n').map((l) => ' ' + l).join('\n')
33
+
34
+ // Terminal control. ?1049 = alt screen, ?25 = cursor, ?2026 = synchronized output.
35
+ const ENTER = '\x1b[?1049h\x1b[?25l'
36
+ const LEAVE = '\x1b[?25h\x1b[?1049l'
37
+ /** Atomic, flicker-free frame: sync-begin → home → per-line clear-to-EOL → clear-below → sync-end. */
38
+ function paint(content: string): void {
39
+ const lines = content.split('\n')
40
+ stdout.write('\x1b[?2026h\x1b[H' + lines.map((l) => l + '\x1b[K').join('\n') + '\x1b[J\x1b[?2026l')
41
+ }
42
+
43
+ export function registerTuiCommands(program: Command, _config: DragonConfig) {
44
+ program
45
+ .command('tui')
46
+ .description('Live operator command-center (press q to quit)')
47
+ .action(async () => {
48
+ const skills = loadSkillLibrary().count
49
+ const ollama = ['/usr/local/bin/ollama', '/usr/bin/ollama', join(homedir(), '.local/bin/ollama')].some(existsSync)
50
+ const wyrmReady = [process.env.WYRM_MCP_BIN, join(homedir(), '.npm-global/bin/wyrm-mcp')].filter(Boolean).some((p) => existsSync(p as string))
51
+ let frame = 0
52
+
53
+ const draw = () => {
54
+ const cols = Math.max(64, Math.min(stdout.columns || 100, 124))
55
+ const inner = cols - 4
56
+ const wide = cols >= 92
57
+ const leftW = wide ? Math.min(44, Math.floor(inner / 2) - 2) : inner
58
+ const rightW = wide ? inner - leftW - 3 : inner
59
+ const actMax = (wide ? rightW : inner) - 8
60
+ const pulse = emerald(PULSE[frame % PULSE.length])
61
+
62
+ const cfg = loadConfig()
63
+ const a = resolveAuth()
64
+ const active = resolveProvider()
65
+ const ready: Record<string, boolean> = {
66
+ claude: !!(process.env.ANTHROPIC_API_KEY || cfg.brain?.keys?.anthropic),
67
+ worker: a.mode !== 'none', local: ollama,
68
+ openai: !!(process.env.OPENAI_API_KEY || cfg.brain?.keys?.openai),
69
+ custom: !!(process.env.DRAGON_OPENAI_BASE || cfg.brain?.customBaseURL), ghost: false,
70
+ }
71
+ let diskFrac = 0
72
+ let diskFree = '—'
73
+ try { const st = statfsSync(process.cwd()); const free = st.bavail * st.bsize; const total = st.blocks * st.bsize; diskFrac = total ? 1 - free / total : 0; diskFree = `${(free / 1e9).toFixed(0)}G free` } catch { /* skip */ }
74
+
75
+ let traces = 0
76
+ let recent: string[] = []
77
+ const hours = new Array(24).fill(0)
78
+ try {
79
+ const lines = readFileSync(join(homedir(), '.dragon', 'traces', `${new Date().toISOString().slice(0, 10)}.jsonl`), 'utf-8').trim().split('\n').filter(Boolean)
80
+ traces = lines.length
81
+ for (const l of lines) { try { const t = JSON.parse(l); hours[new Date(t.ts).getHours()]++ } catch { /* skip */ } }
82
+ recent = lines.slice(-3).map((l) => { try { return (JSON.parse(l).prompt || '').replace(/\s+/g, ' ').trim() } catch { return '' } }).filter(Boolean)
83
+ } catch { /* none today */ }
84
+
85
+ // ── hero ──
86
+ const hero = joinColumns(
87
+ SIGIL.map((l) => chrome(l)).join('\n'),
88
+ ['', chrome('G H O S T P R O T O C O L'), `${C.faint('▌')} ${emerald('OPERATOR CONSOLE')} ${C.faint('//')} ${C.info('DRAGON v' + VER)}`, C.faint('on-host coding + ops agent · sovereign-capable · ghosts.lk'), ''].join('\n'),
89
+ SIGIL_W,
90
+ )
91
+
92
+ // ── panels ──
93
+ const sysLines = [
94
+ `${C.faint('brain ')} ${dot(ready[active] ?? false)} ${C.info(active)}`,
95
+ `${C.faint('auth ')} ${dot(a.mode !== 'none')} ${a.mode === 'none' ? C.faint('signed out') : C.info(a.email || a.mode)}`,
96
+ `${C.faint('memory')} ${dot(wyrmReady)} ${wyrmReady ? C.info('Wyrm') : C.faint('—')} ${C.faint('ollama')} ${dot(ollama)}`,
97
+ `${C.faint('skills')} ${C.info(String(skills))}`,
98
+ `${C.faint('disk ')} ${gauge(diskFrac, 12)} ${C.faint(diskFree)}`,
99
+ ]
100
+ const chartW = Math.max(16, Math.min(actMax, 40))
101
+ const peak = Math.max(...hours, 1)
102
+ const actLines = [
103
+ ...(recent.length ? recent.map((r) => `${C.faint('›')} ${C.info(r.slice(0, actMax))}`) : [C.faint('no agent activity today')]),
104
+ '',
105
+ `${C.faint('24h traces')} ${C.faint('· peak')} ${emerald(String(peak))}`,
106
+ ...brailleChart(hours, { width: chartW, height: 3, area: true }),
107
+ `${pulse} ${C.faint(`listening · ${traces} traces today`)}`,
108
+ ]
109
+
110
+ const body = wide
111
+ ? joinColumns(panel(sysLines, { title: chrome('SYSTEM'), width: leftW }), panel(actLines, { title: chrome('SIGNAL'), width: rightW }), leftW)
112
+ : panel(sysLines, { title: chrome('SYSTEM'), width: inner }) + '\n' + panel(actLines, { title: chrome('SIGNAL'), width: inner })
113
+
114
+ const bar = ` ${pulse} ${C.faint('SYS')} ${emerald('OK')} ${C.faint('· brain')} ${C.info(active)} ${C.faint('· skills')} ${C.info(String(skills))} ${C.faint('·')} ${C.info(new Date().toLocaleTimeString())} ${C.faint('· q quit')}`
115
+
116
+ paint('\n' + indent(hero) + '\n ' + chromeRule(inner) + '\n\n' + indent(body) + '\n\n' + bar)
117
+ }
118
+
119
+ stdout.write(ENTER)
120
+ draw()
121
+ const iv = setInterval(() => { frame++; draw() }, 1000)
122
+ const quit = () => { clearInterval(iv); if (stdin.isTTY) { try { stdin.setRawMode(false) } catch { /* ignore */ } } stdout.write(LEAVE); process.exit(0) }
123
+ process.on('SIGINT', quit)
124
+ if (stdin.isTTY) { stdin.setRawMode(true); stdin.resume(); stdin.on('data', (d) => { const s = d.toString(); if (s === 'q' || s === 'Q' || d[0] === 3) quit() }) }
125
+ })
126
+ }