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.
- package/.github/workflows/ci.yml +23 -0
- package/CHANGELOG.md +96 -0
- package/README.md +193 -0
- package/bootstrap.ps1 +83 -0
- package/bootstrap.sh +71 -0
- package/dist/agent/loop.d.ts +68 -0
- package/dist/agent/loop.d.ts.map +1 -0
- package/dist/agent/loop.js +135 -0
- package/dist/agent/mcp.d.ts +33 -0
- package/dist/agent/mcp.d.ts.map +1 -0
- package/dist/agent/mcp.js +107 -0
- package/dist/agent/session.d.ts +16 -0
- package/dist/agent/session.d.ts.map +1 -0
- package/dist/agent/session.js +55 -0
- package/dist/agent/skills.d.ts +36 -0
- package/dist/agent/skills.d.ts.map +1 -0
- package/dist/agent/skills.js +153 -0
- package/dist/agent/stack.d.ts +21 -0
- package/dist/agent/stack.d.ts.map +1 -0
- package/dist/agent/stack.js +158 -0
- package/dist/agent/task.d.ts +21 -0
- package/dist/agent/task.d.ts.map +1 -0
- package/dist/agent/task.js +45 -0
- package/dist/agent/tools.d.ts +44 -0
- package/dist/agent/tools.d.ts.map +1 -0
- package/dist/agent/tools.js +262 -0
- package/dist/agent/trace.d.ts +34 -0
- package/dist/agent/trace.d.ts.map +1 -0
- package/dist/agent/trace.js +72 -0
- package/dist/agent.d.ts +46 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +103 -0
- package/dist/auth.d.ts +74 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +116 -0
- package/dist/brain/anthropic.d.ts +19 -0
- package/dist/brain/anthropic.d.ts.map +1 -0
- package/dist/brain/anthropic.js +74 -0
- package/dist/brain/claude-cli.d.ts +20 -0
- package/dist/brain/claude-cli.d.ts.map +1 -0
- package/dist/brain/claude-cli.js +79 -0
- package/dist/brain/ghost-ember.d.ts +28 -0
- package/dist/brain/ghost-ember.d.ts.map +1 -0
- package/dist/brain/ghost-ember.js +97 -0
- package/dist/brain/index.d.ts +22 -0
- package/dist/brain/index.d.ts.map +1 -0
- package/dist/brain/index.js +95 -0
- package/dist/brain/openai-compat.d.ts +21 -0
- package/dist/brain/openai-compat.d.ts.map +1 -0
- package/dist/brain/openai-compat.js +119 -0
- package/dist/brain/router/classify.d.ts +23 -0
- package/dist/brain/router/classify.d.ts.map +1 -0
- package/dist/brain/router/classify.js +160 -0
- package/dist/brain/router/execute.d.ts +23 -0
- package/dist/brain/router/execute.d.ts.map +1 -0
- package/dist/brain/router/execute.js +84 -0
- package/dist/brain/router/index.d.ts +26 -0
- package/dist/brain/router/index.d.ts.map +1 -0
- package/dist/brain/router/index.js +118 -0
- package/dist/brain/router/routing-memory.d.ts +27 -0
- package/dist/brain/router/routing-memory.d.ts.map +1 -0
- package/dist/brain/router/routing-memory.js +77 -0
- package/dist/brain/router/select.d.ts +32 -0
- package/dist/brain/router/select.d.ts.map +1 -0
- package/dist/brain/router/select.js +146 -0
- package/dist/brain/router/two-hop.d.ts +23 -0
- package/dist/brain/router/two-hop.d.ts.map +1 -0
- package/dist/brain/router/two-hop.js +39 -0
- package/dist/brain/router/verify.d.ts +37 -0
- package/dist/brain/router/verify.d.ts.map +1 -0
- package/dist/brain/router/verify.js +111 -0
- package/dist/brain/types.d.ts +55 -0
- package/dist/brain/types.d.ts.map +1 -0
- package/dist/brain/types.js +16 -0
- package/dist/brain/worker.d.ts +27 -0
- package/dist/brain/worker.d.ts.map +1 -0
- package/dist/brain/worker.js +71 -0
- package/dist/commands/ai.d.ts +24 -0
- package/dist/commands/ai.d.ts.map +1 -0
- package/dist/commands/ai.js +137 -0
- package/dist/commands/alerts.d.ts +19 -0
- package/dist/commands/alerts.d.ts.map +1 -0
- package/dist/commands/alerts.js +114 -0
- package/dist/commands/billing.d.ts +13 -0
- package/dist/commands/billing.d.ts.map +1 -0
- package/dist/commands/billing.js +55 -0
- package/dist/commands/chat.d.ts +22 -0
- package/dist/commands/chat.d.ts.map +1 -0
- package/dist/commands/chat.js +422 -0
- package/dist/commands/config.d.ts +18 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +136 -0
- package/dist/commands/doctor.d.ts +11 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +73 -0
- package/dist/commands/global.d.ts +11 -0
- package/dist/commands/global.d.ts.map +1 -0
- package/dist/commands/global.js +253 -0
- package/dist/commands/keep.d.ts +12 -0
- package/dist/commands/keep.d.ts.map +1 -0
- package/dist/commands/keep.js +58 -0
- package/dist/commands/lifecycle.d.ts +17 -0
- package/dist/commands/lifecycle.d.ts.map +1 -0
- package/dist/commands/lifecycle.js +267 -0
- package/dist/commands/login.d.ts +16 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +234 -0
- package/dist/commands/maintenance.d.ts +12 -0
- package/dist/commands/maintenance.d.ts.map +1 -0
- package/dist/commands/maintenance.js +76 -0
- package/dist/commands/mcp.d.ts +16 -0
- package/dist/commands/mcp.d.ts.map +1 -0
- package/dist/commands/mcp.js +56 -0
- package/dist/commands/memory.d.ts +13 -0
- package/dist/commands/memory.d.ts.map +1 -0
- package/dist/commands/memory.js +218 -0
- package/dist/commands/osint.d.ts +14 -0
- package/dist/commands/osint.d.ts.map +1 -0
- package/dist/commands/osint.js +161 -0
- package/dist/commands/pentest.d.ts +13 -0
- package/dist/commands/pentest.d.ts.map +1 -0
- package/dist/commands/pentest.js +131 -0
- package/dist/commands/scale.d.ts +14 -0
- package/dist/commands/scale.d.ts.map +1 -0
- package/dist/commands/scale.js +191 -0
- package/dist/commands/serve.d.ts +16 -0
- package/dist/commands/serve.d.ts.map +1 -0
- package/dist/commands/serve.js +167 -0
- package/dist/commands/tui.d.ts +17 -0
- package/dist/commands/tui.d.ts.map +1 -0
- package/dist/commands/tui.js +138 -0
- package/dist/commands/wyrm.d.ts +20 -0
- package/dist/commands/wyrm.d.ts.map +1 -0
- package/dist/commands/wyrm.js +274 -0
- package/dist/config.d.ts +67 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +54 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +85 -0
- package/dist/manifest.d.ts +31 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +83 -0
- package/dist/ui.d.ts +57 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +174 -0
- package/dist/utils.d.ts +33 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +155 -0
- package/dist/wyrm/mcp.d.ts +37 -0
- package/dist/wyrm/mcp.d.ts.map +1 -0
- package/dist/wyrm/mcp.js +137 -0
- package/docs/SYSTEM-PREMORTEM.md +397 -0
- package/dragon-manifest.toml +241 -0
- package/dragon.py +177 -0
- package/install/launchd/lk.ghosts.dragonkeep.plist +57 -0
- package/install/systemd/dragonkeep.service +40 -0
- package/media/dragon-silver-lockup.svg +931 -0
- package/media/dragon-silver-mark.svg +931 -0
- package/media/dragon-silver.png +0 -0
- package/package.json +45 -0
- package/specs/001-godmode/constitution.md +54 -0
- package/specs/001-godmode/plan.md +30 -0
- package/specs/001-godmode/spec.md +64 -0
- package/specs/001-godmode/tasks.md +35 -0
- package/specs/002-premortem-positioning/premortem.md +211 -0
- package/src/agent/loop.ts +165 -0
- package/src/agent/mcp.ts +92 -0
- package/src/agent/session.ts +48 -0
- package/src/agent/skills.ts +138 -0
- package/src/agent/stack.ts +154 -0
- package/src/agent/task.ts +55 -0
- package/src/agent/tools.ts +255 -0
- package/src/agent/trace.ts +76 -0
- package/src/agent.ts +114 -0
- package/src/auth.ts +133 -0
- package/src/brain/anthropic.ts +83 -0
- package/src/brain/claude-cli.ts +78 -0
- package/src/brain/ghost-ember.ts +94 -0
- package/src/brain/index.ts +99 -0
- package/src/brain/openai-compat.ts +115 -0
- package/src/brain/router/classify.ts +167 -0
- package/src/brain/router/execute.ts +80 -0
- package/src/brain/router/index.ts +125 -0
- package/src/brain/router/routing-memory.ts +71 -0
- package/src/brain/router/select.ts +156 -0
- package/src/brain/router/two-hop.ts +62 -0
- package/src/brain/router/verify.ts +123 -0
- package/src/brain/types.ts +61 -0
- package/src/brain/worker.ts +72 -0
- package/src/commands/ai.ts +144 -0
- package/src/commands/alerts.ts +131 -0
- package/src/commands/billing.ts +59 -0
- package/src/commands/chat.ts +318 -0
- package/src/commands/config.ts +137 -0
- package/src/commands/doctor.ts +71 -0
- package/src/commands/global.ts +256 -0
- package/src/commands/keep.ts +67 -0
- package/src/commands/lifecycle.ts +273 -0
- package/src/commands/login.ts +184 -0
- package/src/commands/maintenance.ts +54 -0
- package/src/commands/mcp.ts +57 -0
- package/src/commands/memory.ts +229 -0
- package/src/commands/osint.ts +171 -0
- package/src/commands/pentest.ts +140 -0
- package/src/commands/scale.ts +185 -0
- package/src/commands/serve.ts +171 -0
- package/src/commands/tui.ts +126 -0
- package/src/commands/wyrm.ts +269 -0
- package/src/config.ts +93 -0
- package/src/index.ts +92 -0
- package/src/manifest.ts +104 -0
- package/src/ui.ts +188 -0
- package/src/utils.ts +153 -0
- package/src/wyrm/mcp.ts +130 -0
- package/test/auth.test.ts +70 -0
- package/test/brain.test.ts +39 -0
- package/test/security.test.ts +104 -0
- package/test/skills.test.ts +38 -0
- package/test/ui.test.ts +46 -0
- package/tsconfig.json +19 -0
- package/worker/package-lock.json +1527 -0
- package/worker/package.json +17 -0
- package/worker/src/index.ts +76 -0
- package/worker/tsconfig.json +15 -0
- package/worker/wrangler.toml +26 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dragon ai — Bridge the dragon stack to AI clients (Claude Code,
|
|
3
|
+
* Codex, Copilot CLI, Aider, Cursor, etc.)
|
|
4
|
+
*
|
|
5
|
+
* ai context <target> — Print a compact context dump suitable
|
|
6
|
+
* for splicing into any LLM system prompt
|
|
7
|
+
* ai mcp-config — Print Claude Desktop MCP JSON config
|
|
8
|
+
* ai brief <target> — Print engagement brief Markdown
|
|
9
|
+
* ai prompt <target> — Print operator-ready prompt with
|
|
10
|
+
* memory context + suggested actions
|
|
11
|
+
*
|
|
12
|
+
* Designed so an operator running Claude Code in another window can do:
|
|
13
|
+
*
|
|
14
|
+
* $ dragon ai prompt upalis.com | claude
|
|
15
|
+
*
|
|
16
|
+
* and Claude picks up the engagement context immediately.
|
|
17
|
+
*
|
|
18
|
+
* Copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
|
|
19
|
+
* Author: Ryan Sebastian <ryan@ghosts.lk>
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { Command } from 'commander'
|
|
23
|
+
import type { DragonConfig } from '../config.js'
|
|
24
|
+
import { fetchJSON, label, error, info } from '../utils.js'
|
|
25
|
+
import chalk from 'chalk'
|
|
26
|
+
|
|
27
|
+
function api(config: DragonConfig, path: string): string {
|
|
28
|
+
const port = config.products.pentest.controlPort ?? 4091
|
|
29
|
+
return `http://localhost:${port}${path}`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface Suggestion {
|
|
33
|
+
verb: string
|
|
34
|
+
title: string
|
|
35
|
+
rationale: string
|
|
36
|
+
command: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function registerAiCommands(program: Command, config: DragonConfig) {
|
|
40
|
+
const ai = program.command('ai').description('Bridge the dragon stack to AI clients')
|
|
41
|
+
|
|
42
|
+
// --- context ---
|
|
43
|
+
ai.command('context <target>')
|
|
44
|
+
.description('Print compact context for splicing into an LLM system prompt')
|
|
45
|
+
.action(async (target) => {
|
|
46
|
+
try {
|
|
47
|
+
const port = config.products.pentest.controlPort ?? 4091
|
|
48
|
+
const res = await fetch(`http://localhost:${port}/v1/memory/target/${encodeURIComponent(target)}/context`)
|
|
49
|
+
if (!res.ok) { error(`HTTP ${res.status}`); return }
|
|
50
|
+
process.stdout.write(await res.text())
|
|
51
|
+
} catch (e) {
|
|
52
|
+
error(String(e))
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
// --- prompt ---
|
|
57
|
+
ai.command('prompt <target>')
|
|
58
|
+
.description('Operator-ready prompt block: identity + memory + suggested actions')
|
|
59
|
+
.action(async (target) => {
|
|
60
|
+
try {
|
|
61
|
+
const port = config.products.pentest.controlPort ?? 4091
|
|
62
|
+
const [ctxR, sugR] = await Promise.all([
|
|
63
|
+
fetch(`http://localhost:${port}/v1/memory/target/${encodeURIComponent(target)}/context`),
|
|
64
|
+
fetchJSON<{ suggestions: Suggestion[] }>(api(config, `/v1/memory/target/${encodeURIComponent(target)}/copilot`)),
|
|
65
|
+
])
|
|
66
|
+
const ctx = ctxR.ok ? await ctxR.text() : "# No prior memory."
|
|
67
|
+
process.stdout.write([
|
|
68
|
+
"You are operating inside Ghost Protocol's offensive-security stack.",
|
|
69
|
+
"Target context follows. Cite scan_ids when referring to prior findings.",
|
|
70
|
+
"",
|
|
71
|
+
"---",
|
|
72
|
+
"",
|
|
73
|
+
ctx,
|
|
74
|
+
"",
|
|
75
|
+
"## Suggested next actions",
|
|
76
|
+
"",
|
|
77
|
+
...sugR.suggestions.map((s) => `- **${s.title}** — ${s.rationale}\n Command: \`${s.command}\``),
|
|
78
|
+
"",
|
|
79
|
+
"---",
|
|
80
|
+
"",
|
|
81
|
+
"When the operator asks a question, prefer concrete actions from above.",
|
|
82
|
+
"Never invent findings; if memory is empty, say so and propose a baseline scan.",
|
|
83
|
+
"",
|
|
84
|
+
].join("\n"))
|
|
85
|
+
} catch (e) {
|
|
86
|
+
error(String(e))
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// --- brief ---
|
|
91
|
+
ai.command('brief <target>')
|
|
92
|
+
.description('Print engagement-brief Markdown for paste into any client')
|
|
93
|
+
.action(async (target) => {
|
|
94
|
+
try {
|
|
95
|
+
const port = config.products.pentest.controlPort ?? 4091
|
|
96
|
+
const res = await fetch(`http://localhost:${port}/v1/memory/target/${encodeURIComponent(target)}/brief`)
|
|
97
|
+
if (!res.ok) { error(`HTTP ${res.status}`); return }
|
|
98
|
+
process.stdout.write(await res.text())
|
|
99
|
+
} catch (e) {
|
|
100
|
+
error(String(e))
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
// --- mcp-config ---
|
|
105
|
+
ai.command('mcp-config')
|
|
106
|
+
.description('Print Claude Desktop MCP server configuration')
|
|
107
|
+
.option('--name <name>', 'Server profile name', 'dragon-stack')
|
|
108
|
+
.action((opts) => {
|
|
109
|
+
const cfg = {
|
|
110
|
+
mcpServers: {
|
|
111
|
+
"phantom-memory": {
|
|
112
|
+
command: "phantom-memory-mcp",
|
|
113
|
+
env: { PD_CONTROL_API: `http://localhost:${config.products.pentest.controlPort ?? 4091}` },
|
|
114
|
+
},
|
|
115
|
+
"dragonkeep": {
|
|
116
|
+
command: "dragonkeep-mcp",
|
|
117
|
+
},
|
|
118
|
+
"dragonnet": {
|
|
119
|
+
command: "dragonnet-mcp",
|
|
120
|
+
env: { DRAGONNET_API: `http://localhost:${config.products.net.apiPort ?? 4080}` },
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
}
|
|
124
|
+
console.log(label('Dragon AI'), `MCP config for ${chalk.bold(opts.name)}:\n`)
|
|
125
|
+
console.log(JSON.stringify(cfg, null, 2))
|
|
126
|
+
console.log()
|
|
127
|
+
info('Linux: paste into ~/.config/Claude/claude_desktop_config.json')
|
|
128
|
+
info('macOS: ~/Library/Application Support/Claude/claude_desktop_config.json')
|
|
129
|
+
info('Restart Claude Desktop after editing.')
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
// --- skills ---
|
|
133
|
+
ai.command('skills')
|
|
134
|
+
.description('Print Claude Code skill paths and an install hint')
|
|
135
|
+
.action(() => {
|
|
136
|
+
const skills = [
|
|
137
|
+
"scan", "memory", "osint", "chronicle", "triage",
|
|
138
|
+
]
|
|
139
|
+
console.log(label('Dragon AI'), 'Claude Code skills available:\n')
|
|
140
|
+
skills.forEach((s) => console.log(` /${s.padEnd(12)} ~/.claude/skills/dragon-${s}/SKILL.md`))
|
|
141
|
+
console.log()
|
|
142
|
+
info('These are pre-installed in ~/.copilot/skills/ — symlinked into ~/.claude/skills/.')
|
|
143
|
+
})
|
|
144
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dragon alerts — Webhook alert pipeline (Slack / Discord / generic)
|
|
3
|
+
*
|
|
4
|
+
* alerts list
|
|
5
|
+
* alerts add <url> --label NAME --kind slack|discord|generic
|
|
6
|
+
* alerts remove <id>
|
|
7
|
+
* alerts test — fire a test event to every webhook
|
|
8
|
+
*
|
|
9
|
+
* Backed by PhantomDragon Control's /v1/alerts/webhooks endpoints. Any
|
|
10
|
+
* defensive Critical/High finding (DragonKeep → Phantom Memory) auto-
|
|
11
|
+
* fan-outs to every webhook that has on_critical / on_high enabled.
|
|
12
|
+
*
|
|
13
|
+
* Copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
|
|
14
|
+
* Author: Ryan Sebastian <ryan@ghosts.lk>
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { Command } from 'commander'
|
|
18
|
+
import type { DragonConfig } from '../config.js'
|
|
19
|
+
import { fetchJSON, label, success, error, info, table } from '../utils.js'
|
|
20
|
+
import chalk from 'chalk'
|
|
21
|
+
|
|
22
|
+
function api(config: DragonConfig, path: string): string {
|
|
23
|
+
const port = config.products.pentest.controlPort ?? 4091
|
|
24
|
+
return `http://localhost:${port}${path}`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface Webhook {
|
|
28
|
+
id: string
|
|
29
|
+
label: string
|
|
30
|
+
url: string
|
|
31
|
+
kind: string
|
|
32
|
+
on_critical: boolean
|
|
33
|
+
on_high: boolean
|
|
34
|
+
on_scan_complete: boolean
|
|
35
|
+
enabled: boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function postJSON<T>(url: string, body: unknown): Promise<T> {
|
|
39
|
+
const res = await fetch(url, {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
headers: { 'Content-Type': 'application/json' },
|
|
42
|
+
body: JSON.stringify(body),
|
|
43
|
+
})
|
|
44
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`)
|
|
45
|
+
return res.json() as Promise<T>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function del(url: string): Promise<void> {
|
|
49
|
+
const res = await fetch(url, { method: 'DELETE' })
|
|
50
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function registerAlertsCommands(program: Command, config: DragonConfig) {
|
|
54
|
+
const alerts = program
|
|
55
|
+
.command('alerts')
|
|
56
|
+
.description('Webhook alert pipeline (Slack / Discord / generic)')
|
|
57
|
+
|
|
58
|
+
alerts
|
|
59
|
+
.command('list')
|
|
60
|
+
.description('List configured webhooks')
|
|
61
|
+
.action(async () => {
|
|
62
|
+
try {
|
|
63
|
+
const d = await fetchJSON<{ webhooks: Webhook[] }>(api(config, '/v1/alerts/webhooks'))
|
|
64
|
+
if (d.webhooks.length === 0) {
|
|
65
|
+
info('No webhooks configured. Try: dragon alerts add <url> --label slack')
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
console.log(label('Alerts'), 'Webhooks:\n')
|
|
69
|
+
table(d.webhooks.map((w) => ({
|
|
70
|
+
ID: w.id,
|
|
71
|
+
Label: w.label,
|
|
72
|
+
Kind: w.kind,
|
|
73
|
+
'Crit/High/Scan': `${w.on_critical ? '✓' : '·'}${w.on_high ? '✓' : '·'}${w.on_scan_complete ? '✓' : '·'}`,
|
|
74
|
+
Enabled: w.enabled ? 'yes' : 'no',
|
|
75
|
+
URL: w.url.slice(0, 50),
|
|
76
|
+
})))
|
|
77
|
+
} catch (e) {
|
|
78
|
+
error(String(e))
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
alerts
|
|
83
|
+
.command('add <url>')
|
|
84
|
+
.description('Register a new webhook')
|
|
85
|
+
.option('--label <label>', 'Display label', 'webhook')
|
|
86
|
+
.option('--kind <kind>', 'slack | discord | generic', 'generic')
|
|
87
|
+
.option('--on-high', 'Also fire on HIGH (default: CRITICAL only)')
|
|
88
|
+
.option('--on-scan-complete', 'Fire on scan completion events')
|
|
89
|
+
.action(async (url, opts) => {
|
|
90
|
+
try {
|
|
91
|
+
const created = await postJSON<Webhook>(api(config, '/v1/alerts/webhooks'), {
|
|
92
|
+
label: opts.label,
|
|
93
|
+
url,
|
|
94
|
+
kind: opts.kind,
|
|
95
|
+
on_critical: true,
|
|
96
|
+
on_high: !!opts.onHigh,
|
|
97
|
+
on_scan_complete: !!opts.onScanComplete,
|
|
98
|
+
enabled: true,
|
|
99
|
+
})
|
|
100
|
+
success(`Registered webhook ${chalk.bold(created.id)} (${created.kind})`)
|
|
101
|
+
} catch (e) {
|
|
102
|
+
error(String(e))
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
alerts
|
|
107
|
+
.command('remove <id>')
|
|
108
|
+
.description('Delete a webhook')
|
|
109
|
+
.action(async (id) => {
|
|
110
|
+
try {
|
|
111
|
+
await del(api(config, `/v1/alerts/webhooks/${encodeURIComponent(id)}`))
|
|
112
|
+
success(`Removed webhook ${id}`)
|
|
113
|
+
} catch (e) {
|
|
114
|
+
error(String(e))
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
alerts
|
|
119
|
+
.command('test')
|
|
120
|
+
.description('Fire a test CRITICAL event to every webhook')
|
|
121
|
+
.action(async () => {
|
|
122
|
+
try {
|
|
123
|
+
const r = await postJSON<{ fired: number; webhooks_total: number }>(
|
|
124
|
+
api(config, '/v1/alerts/test'), {},
|
|
125
|
+
)
|
|
126
|
+
success(`Fired to ${r.fired} of ${r.webhooks_total} webhook(s)`)
|
|
127
|
+
} catch (e) {
|
|
128
|
+
error(String(e))
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dragon billing — License management (Paddle / DragonSeal)
|
|
3
|
+
*
|
|
4
|
+
* billing licenses — list active product licences
|
|
5
|
+
* billing check <product> — quick check whether <product> is licensed
|
|
6
|
+
*
|
|
7
|
+
* Copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
|
|
8
|
+
* Author: Ryan Sebastian <ryan@ghosts.lk>
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Command } from 'commander'
|
|
12
|
+
import type { DragonConfig } from '../config.js'
|
|
13
|
+
import { fetchJSON, label, error, info } from '../utils.js'
|
|
14
|
+
import chalk from 'chalk'
|
|
15
|
+
|
|
16
|
+
function api(config: DragonConfig, path: string): string {
|
|
17
|
+
const port = config.products.pentest.controlPort ?? 4091
|
|
18
|
+
return `http://localhost:${port}${path}`
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function registerBillingCommands(program: Command, config: DragonConfig) {
|
|
22
|
+
const billing = program.command('billing').description('License management — Paddle / DragonSeal')
|
|
23
|
+
|
|
24
|
+
billing.command('licenses')
|
|
25
|
+
.description('Active product licences on this host')
|
|
26
|
+
.option('--email <email>', 'Restrict to a specific operator email')
|
|
27
|
+
.action(async (opts) => {
|
|
28
|
+
try {
|
|
29
|
+
const params = new URLSearchParams()
|
|
30
|
+
if (opts.email) params.set('email', opts.email)
|
|
31
|
+
const d = await fetchJSON<{ licenses: string[] }>(api(config, `/v1/billing/licenses?${params}`))
|
|
32
|
+
if (d.licenses.length === 0) {
|
|
33
|
+
info('No active licences. Free tier active for every product.')
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
console.log(label('Licenses'), 'Active products:\n')
|
|
37
|
+
d.licenses.forEach((p) => console.log(` ${chalk.green('●')} ${p}`))
|
|
38
|
+
} catch (e) {
|
|
39
|
+
error(String(e))
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
billing.command('check <product>')
|
|
44
|
+
.description('Check whether <product> is licensed')
|
|
45
|
+
.option('--email <email>', 'Restrict to a specific operator email')
|
|
46
|
+
.action(async (product, opts) => {
|
|
47
|
+
try {
|
|
48
|
+
const params = new URLSearchParams({ product })
|
|
49
|
+
if (opts.email) params.set('email', opts.email)
|
|
50
|
+
const d = await fetchJSON<{ product: string; licensed: boolean }>(
|
|
51
|
+
api(config, `/v1/billing/check?${params}`),
|
|
52
|
+
)
|
|
53
|
+
const sym = d.licensed ? chalk.green('✓ licensed') : chalk.yellow('· free tier')
|
|
54
|
+
console.log(`${product.padEnd(28)} ${sym}`)
|
|
55
|
+
} catch (e) {
|
|
56
|
+
error(String(e))
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dragon chat / dragon ask — the Dragon agent: a Claude-Code-style terminal
|
|
3
|
+
* coding agent that runs tools locally, reasons with a pluggable brain (Claude
|
|
4
|
+
* by default), and is wired into Wyrm long-term memory by default.
|
|
5
|
+
*
|
|
6
|
+
* dragon chat interactive agent — edits files, runs commands,
|
|
7
|
+
* remembers across sessions via Wyrm
|
|
8
|
+
* dragon ask "…" one-shot (read-only unless --auto); pipeable
|
|
9
|
+
*
|
|
10
|
+
* --brain claude|openai|local reasoning model (default: config/claude)
|
|
11
|
+
* --model <id> override the model
|
|
12
|
+
* --no-wyrm disable Wyrm memory for this run
|
|
13
|
+
* --no-portal don't expose the hosted account assistant tool
|
|
14
|
+
* --auto auto-approve file writes + shell (use with care)
|
|
15
|
+
* --cwd <dir> run the agent against another directory
|
|
16
|
+
*
|
|
17
|
+
* Copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { Command } from 'commander'
|
|
21
|
+
import { loadConfig, saveConfig, type DragonConfig } from '../config.js'
|
|
22
|
+
import { C, error, info, success } from '../utils.js'
|
|
23
|
+
import { saveSession, loadLastSession, exportMarkdown } from '../agent/session.js'
|
|
24
|
+
import { panel, chrome, kv, statusDot, tidyPath } from '../ui.js'
|
|
25
|
+
import { getBrain, BrainConfigError, type Provider } from '../brain/index.js'
|
|
26
|
+
import type { Brain, BrainMessage, ToolSpec } from '../brain/types.js'
|
|
27
|
+
import { Wyrm } from '../wyrm/mcp.js'
|
|
28
|
+
import { BWRAP, type ToolContext } from '../agent/tools.js'
|
|
29
|
+
import { runAgent, buildSystemPrompt, buildToolSpecs, type AgentDeps, type AgentRender, type PortalTool } from '../agent/loop.js'
|
|
30
|
+
import { recordTurn, traceEnabled } from '../agent/trace.js'
|
|
31
|
+
import { loadSkillLibrary, type SkillLibrary } from '../agent/skills.js'
|
|
32
|
+
import { makeStackTools } from '../agent/stack.js'
|
|
33
|
+
import { loadMcpHub } from '../agent/mcp.js'
|
|
34
|
+
import { makeTaskTool } from '../agent/task.js'
|
|
35
|
+
import { resolveAuth } from '../auth.js'
|
|
36
|
+
import { streamAgent, AgentError, SURFACES } from '../agent.js'
|
|
37
|
+
import { readFileSync, existsSync } from 'node:fs'
|
|
38
|
+
import { createInterface, type Interface } from 'node:readline/promises'
|
|
39
|
+
import { stdin, stdout } from 'node:process'
|
|
40
|
+
|
|
41
|
+
function expandFileRefs(text: string): string {
|
|
42
|
+
return text.replace(/@(\S+)/g, (match, path) => {
|
|
43
|
+
if (path === '-' || !existsSync(path)) return match
|
|
44
|
+
try { return `\n\n--- ${path} ---\n${readFileSync(path, 'utf-8')}\n--- end ${path} ---\n\n` } catch { return match }
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function readStdinAll(): Promise<string> {
|
|
49
|
+
const chunks: Buffer[] = []
|
|
50
|
+
for await (const c of stdin) chunks.push(c as Buffer)
|
|
51
|
+
return Buffer.concat(chunks).toString('utf-8')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The hosted account.ghosts.lk assistant, exposed to the agent as one tool. */
|
|
55
|
+
function makePortalTool(): PortalTool | null {
|
|
56
|
+
if (resolveAuth().mode === 'none') return null
|
|
57
|
+
const spec: ToolSpec = {
|
|
58
|
+
name: 'portal_ask',
|
|
59
|
+
description: "Ask the Ghost Protocol account portal assistant about the operator's licenses, product catalog, security services/pricing, or to book an engagement. Account/business questions only — not coding.",
|
|
60
|
+
parameters: { type: 'object', properties: { question: { type: 'string' }, surface: { type: 'string', enum: [...SURFACES] } }, required: ['question'] },
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
spec,
|
|
64
|
+
async call(args) {
|
|
65
|
+
const surface = (SURFACES as readonly string[]).includes(String(args.surface)) ? String(args.surface) : 'dashboard'
|
|
66
|
+
try {
|
|
67
|
+
return await streamAgent({ messages: [{ role: 'user', content: String(args.question ?? '') }], surface, onDelta: () => {} })
|
|
68
|
+
} catch (e) {
|
|
69
|
+
return e instanceof AgentError ? `portal error: ${e.message}` : `portal error: ${String(e instanceof Error ? e.message : e)}`
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface SetupOpts { brain?: string; model?: string; wyrm?: boolean; portal?: boolean; cwd?: string; sovereign?: boolean }
|
|
76
|
+
|
|
77
|
+
function hasKey(p: 'anthropic' | 'openai'): boolean {
|
|
78
|
+
const cfg = loadConfig()
|
|
79
|
+
return p === 'anthropic' ? !!(process.env.ANTHROPIC_API_KEY || cfg.brain?.keys?.anthropic) : !!(process.env.OPENAI_API_KEY || cfg.brain?.keys?.openai)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** First-launch model picker (interactive). Persists the choice to config. */
|
|
83
|
+
async function runPicker(): Promise<void> {
|
|
84
|
+
const signedIn = resolveAuth().mode !== 'none'
|
|
85
|
+
const ready = (ok: boolean, need: string) => (ok ? C.accent('● ready') : C.faint('○ ' + need))
|
|
86
|
+
console.log()
|
|
87
|
+
console.log(` ${chrome("CHOOSE DRAGON'S BRAIN")}`)
|
|
88
|
+
console.log(` ${C.faint('Tools always run locally — this only picks the reasoning model.')}`)
|
|
89
|
+
console.log()
|
|
90
|
+
console.log(` ${C.accent('1')} Cloudflare worker ${C.faint('· free · our infra')} ${ready(signedIn, 'needs `dragon login`')}`)
|
|
91
|
+
console.log(` ${C.accent('2')} Claude ${C.faint('· best for code')} ${ready(hasKey('anthropic'), 'needs ANTHROPIC_API_KEY')}`)
|
|
92
|
+
console.log(` ${C.accent('3')} Local Ollama ${C.faint('· private · free')}`)
|
|
93
|
+
console.log(` ${C.accent('4')} OpenAI ${C.faint('· hosted')} ${ready(hasKey('openai'), 'needs OPENAI_API_KEY')}`)
|
|
94
|
+
console.log()
|
|
95
|
+
const rl = createInterface({ input: stdin, output: stdout })
|
|
96
|
+
let ans = ''
|
|
97
|
+
try { ans = (await rl.question(` ${C.accent('▸')} pick ${C.faint('[1-4, default 1]')} `)).trim() } finally { rl.close() }
|
|
98
|
+
const provider: Provider = ({ '1': 'worker', '2': 'claude', '3': 'local', '4': 'openai' } as Record<string, Provider>)[ans] ?? 'worker'
|
|
99
|
+
const cfg = loadConfig(); cfg.brain = { ...(cfg.brain ?? {}), provider }; saveConfig(cfg)
|
|
100
|
+
info(`brain → ${C.accent(provider)}. Change anytime with ${C.info('dragon config brain <name>')}.`)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Resolve the brain: explicit --brain, else configured (with a first-launch picker),
|
|
104
|
+
* else gracefully fall back to the free Cloudflare worker / local Ollama. */
|
|
105
|
+
const HOSTED = ['claude', 'worker', 'openai']
|
|
106
|
+
async function chooseBrain(opts: SetupOpts): Promise<Brain | null> {
|
|
107
|
+
const tryGet = (provider?: string): Brain | null => {
|
|
108
|
+
try { return getBrain({ provider, model: opts.model }) }
|
|
109
|
+
catch (e) { error(e instanceof BrainConfigError ? e.message : String(e instanceof Error ? e.message : e)); return null }
|
|
110
|
+
}
|
|
111
|
+
if (opts.sovereign) {
|
|
112
|
+
// local brain only — nothing leaves the host
|
|
113
|
+
if (opts.brain && HOSTED.includes(opts.brain)) info(`sovereign mode — ignoring hosted brain '${opts.brain}', using local.`)
|
|
114
|
+
return tryGet(opts.brain && !HOSTED.includes(opts.brain) ? opts.brain : 'local')
|
|
115
|
+
}
|
|
116
|
+
if (opts.brain) return tryGet(opts.brain)
|
|
117
|
+
if (!loadConfig().brain?.provider && stdin.isTTY) await runPicker()
|
|
118
|
+
try {
|
|
119
|
+
return getBrain({ model: opts.model })
|
|
120
|
+
} catch (e) {
|
|
121
|
+
if (!(e instanceof BrainConfigError)) { error(String(e instanceof Error ? e.message : e)); return null }
|
|
122
|
+
if (resolveAuth().mode !== 'none') { info(`${e.provider} brain unavailable — using the free Cloudflare brain.`); return getBrain({ provider: 'worker' }) }
|
|
123
|
+
info(`${e.provider} brain unavailable — using local Ollama. (Run \`dragon login\` for the free Cloudflare brain.)`)
|
|
124
|
+
return tryGet('local')
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Build brain + wyrm + portal + system prompt. Returns null after printing on a brain config error. */
|
|
129
|
+
async function setup(opts: SetupOpts, announce: boolean) {
|
|
130
|
+
const brain = await chooseBrain(opts)
|
|
131
|
+
if (!brain) return null
|
|
132
|
+
const cwd = opts.cwd ? (opts.cwd.startsWith('/') ? opts.cwd : `${process.cwd()}/${opts.cwd}`) : process.cwd()
|
|
133
|
+
|
|
134
|
+
let wyrm: Wyrm | null = null
|
|
135
|
+
let primed: string | null = null
|
|
136
|
+
if (opts.wyrm !== false) {
|
|
137
|
+
wyrm = new Wyrm()
|
|
138
|
+
const ok = await wyrm.connect()
|
|
139
|
+
if (ok) primed = await wyrm.prime(cwd)
|
|
140
|
+
else { if (announce) info('Wyrm memory unavailable — continuing without it.'); wyrm = null }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const portal = (opts.portal === false || opts.sovereign) ? null : makePortalTool()
|
|
144
|
+
const skills = loadSkillLibrary()
|
|
145
|
+
const stack = makeStackTools()
|
|
146
|
+
const mcp = await loadMcpHub(loadConfig().mcpServers)
|
|
147
|
+
if (mcp && announce) info(`MCP: ${mcp.serverCount} server(s) connected (${mcp.toolSpecs().length} tools).`)
|
|
148
|
+
const task = makeTaskTool({ brain, skills, cwd })
|
|
149
|
+
const system = buildSystemPrompt({ cwd, wyrm: !!wyrm, portal: !!portal, brainId: `${brain.id}:${brain.model}`, skills: skills.count || undefined, primed })
|
|
150
|
+
return { brain, wyrm, portal, skills, stack, mcp, task, cwd, system, primed }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function banner(s: { brain: { id: string; model: string }; wyrm: Wyrm | null; portal: PortalTool | null; skills?: SkillLibrary | null; cwd: string }, tools: ToolSpec[]): void {
|
|
154
|
+
const lines = [
|
|
155
|
+
kv('brain', C.info(`${s.brain.id}:${s.brain.model}`)),
|
|
156
|
+
kv('cwd', C.info(tidyPath(s.cwd))),
|
|
157
|
+
kv('state', `memory ${statusDot(!!s.wyrm)} ${s.wyrm ? C.info('wyrm') : C.faint('off')} portal ${statusDot(!!s.portal)} skills ${C.info(String(s.skills?.count ?? 0))} tools ${C.info(String(tools.length))}`),
|
|
158
|
+
]
|
|
159
|
+
console.log()
|
|
160
|
+
console.log(panel(lines, { title: chrome('DRAGON AGENT') }))
|
|
161
|
+
console.log(` ${C.faint('/exit · /reset · /brain · /tools · /memory <q> · /auto · /plan · /save · @<file>')}`)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function makeRender(): AgentRender {
|
|
165
|
+
return {
|
|
166
|
+
onAssistantStart() { process.stdout.write(`\n ${C.hot('◆')} `) },
|
|
167
|
+
onDelta(s) { process.stdout.write(s) },
|
|
168
|
+
onToolStart(summary) { process.stdout.write(`\n ${C.faint('⚙ ' + summary)}`) },
|
|
169
|
+
onToolEnd(_summary, preview, ok) { process.stdout.write(` ${ok ? C.accent('✓') : C.critical('✗')} ${C.faint(preview)}\n`) },
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function makeApprove(rl: Interface, state: { auto: boolean; plan: boolean }) {
|
|
174
|
+
return async (summary: string, opts?: { detail?: string; dangerous?: boolean }): Promise<boolean> => {
|
|
175
|
+
if (state.plan) { console.log(` ${C.faint('⊘ plan mode — declined: ' + summary)}`); return false } // read-only
|
|
176
|
+
const dangerous = !!opts?.dangerous
|
|
177
|
+
if (state.auto && !dangerous) return true // --auto covers safe in-cwd edits only — NEVER bash / outside-cwd
|
|
178
|
+
if (opts?.detail) console.log('\n' + C.faint(opts.detail.split('\n').map((l) => ' ' + l).join('\n')))
|
|
179
|
+
const tag = dangerous ? C.critical('?') : C.accent('?')
|
|
180
|
+
const hint = dangerous ? (state.auto ? '[y/N — auto-approve does NOT cover this]' : '[y/N]') : '[y/N/a=always-safe]'
|
|
181
|
+
const ans = (await rl.question(`\n ${tag} ${summary} ${C.faint(hint)} `)).trim().toLowerCase()
|
|
182
|
+
if (ans === 'a' && !dangerous) { state.auto = true; return true }
|
|
183
|
+
return ans === 'y' || ans === 'yes'
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function registerChatCommands(program: Command, _config: DragonConfig) {
|
|
188
|
+
program
|
|
189
|
+
.command('chat')
|
|
190
|
+
.description('Dragon agent — codes, runs tools, remembers via Wyrm (interactive)')
|
|
191
|
+
.option('--brain <provider>', 'reasoning brain: claude | worker | local | openai | ghost')
|
|
192
|
+
.option('--model <id>', 'override the model id')
|
|
193
|
+
.option('--no-wyrm', 'disable Wyrm long-term memory for this session')
|
|
194
|
+
.option('--no-portal', "don't expose the hosted account assistant as a tool")
|
|
195
|
+
.option('--auto', 'auto-approve file writes + shell commands', false)
|
|
196
|
+
.option('--plan', 'read-only: the agent explores but makes no writes/shell', false)
|
|
197
|
+
.option('--sovereign', 'local brain + Wyrm only — nothing leaves the host', false)
|
|
198
|
+
.option('--sandbox', 'run shell in a bwrap jail (cwd writable, rest read-only)', false)
|
|
199
|
+
.option('--no-trace', 'do not record tool-use traces for DragonSpark training')
|
|
200
|
+
.option('--resume', 'resume the previous session', false)
|
|
201
|
+
.option('--cwd <dir>', 'run against a different working directory')
|
|
202
|
+
.action(async (opts: SetupOpts & { auto?: boolean; plan?: boolean; sandbox?: boolean; trace?: boolean; resume?: boolean }) => {
|
|
203
|
+
const s = await setup(opts, true)
|
|
204
|
+
if (!s) { process.exitCode = 1; return }
|
|
205
|
+
|
|
206
|
+
const rl = createInterface({ input: stdin, output: stdout, terminal: stdin.isTTY === true })
|
|
207
|
+
const state = { auto: !!opts.auto, plan: !!opts.plan }
|
|
208
|
+
const toolCtx: ToolContext = { cwd: s.cwd, approve: makeApprove(rl, state), sandbox: !!opts.sandbox }
|
|
209
|
+
const messages: BrainMessage[] = []
|
|
210
|
+
if (opts.resume) {
|
|
211
|
+
const prior = loadLastSession()
|
|
212
|
+
if (prior) { messages.push(...prior); info(`resumed ${prior.length} messages from your last session`) }
|
|
213
|
+
else info('no previous session to resume')
|
|
214
|
+
}
|
|
215
|
+
const deps: AgentDeps = { ...s, toolCtx, messages }
|
|
216
|
+
const render = makeRender()
|
|
217
|
+
|
|
218
|
+
banner(s, buildToolSpecs(s))
|
|
219
|
+
if (state.plan) info('plan mode ON — read-only: the agent explores but makes no writes/shell (/plan to toggle).')
|
|
220
|
+
else if (state.auto) info('auto-approve ON — safe in-cwd writes run without asking (bash still prompts).')
|
|
221
|
+
if (opts.sovereign) info('sovereign mode — local brain + Wyrm only; nothing leaves the host.')
|
|
222
|
+
if (opts.sandbox) info(BWRAP ? 'sandbox ON — shell runs in bwrap (cwd writable, secrets masked, network shared).' : 'sandbox requested but bwrap not found — shell runs unsandboxed (approval still applies).')
|
|
223
|
+
if (traceEnabled(opts.trace === false)) info('flywheel ON — turns logged to ~/.dragon/traces for DragonSpark (--no-trace to disable).')
|
|
224
|
+
|
|
225
|
+
let aborter: AbortController | null = null
|
|
226
|
+
const onSigint = () => { if (aborter) { aborter.abort(); aborter = null } else { void cleanup().then(() => process.exit(0)) } }
|
|
227
|
+
const cleanup = async () => { process.off('SIGINT', onSigint); rl.close(); saveSession(messages, { cwd: s.cwd, brain: `${s.brain.id}:${s.brain.model}` }); await s.wyrm?.close(); await s.mcp?.close() }
|
|
228
|
+
process.on('SIGINT', onSigint)
|
|
229
|
+
|
|
230
|
+
for (;;) {
|
|
231
|
+
let userIn: string
|
|
232
|
+
try { userIn = (await rl.question(`${C.accent('▸')} `)).trim() } catch { break }
|
|
233
|
+
if (!userIn) continue
|
|
234
|
+
|
|
235
|
+
if (userIn === '/exit' || userIn === '/quit') break
|
|
236
|
+
if (userIn === '/reset') { messages.length = 0; state.auto = false; info('conversation reset · auto-approve re-armed'); continue }
|
|
237
|
+
if (userIn === '/auto') { state.auto = !state.auto; if (state.auto) state.plan = false; info(`auto-approve ${state.auto ? 'ON' : 'OFF'}`); continue }
|
|
238
|
+
if (userIn === '/plan') { state.plan = !state.plan; if (state.plan) state.auto = false; info(`plan mode ${state.plan ? 'ON — read-only' : 'OFF'}`); continue }
|
|
239
|
+
if (userIn === '/brain') { console.log(` ${C.faint('brain:')} ${C.info(s.brain.id + ':' + s.brain.model)}`); continue }
|
|
240
|
+
if (userIn === '/tools') { console.log(' ' + buildToolSpecs(s).map((t) => t.name).join(' ')); continue }
|
|
241
|
+
if (userIn === '/clear') { console.clear(); banner(s, buildToolSpecs(s)); continue }
|
|
242
|
+
if (userIn.startsWith('/save')) { success(`transcript → ${exportMarkdown(messages, userIn.slice(5).trim() || undefined)}`); continue }
|
|
243
|
+
if (userIn.startsWith('/memory')) {
|
|
244
|
+
const q = userIn.slice(7).trim()
|
|
245
|
+
if (!s.wyrm) { error('Wyrm is off this session'); continue }
|
|
246
|
+
if (!q) { info('usage: /memory <query>'); continue }
|
|
247
|
+
console.log(C.faint(await s.wyrm.call('wyrm_recall', { query: q })))
|
|
248
|
+
continue
|
|
249
|
+
}
|
|
250
|
+
if (userIn.startsWith('/')) { error(`unknown command ${userIn}`); continue }
|
|
251
|
+
|
|
252
|
+
aborter = new AbortController()
|
|
253
|
+
const before = messages.length
|
|
254
|
+
try {
|
|
255
|
+
await runAgent(deps, expandFileRefs(userIn), render, aborter.signal)
|
|
256
|
+
process.stdout.write('\n\n')
|
|
257
|
+
if (traceEnabled(opts.trace === false)) recordTurn(messages.slice(before), { cwd: s.cwd, brain: `${s.brain.id}:${s.brain.model}`, context: s.primed })
|
|
258
|
+
} catch (e) {
|
|
259
|
+
aborter = null
|
|
260
|
+
if ((e as { name?: string })?.name === 'AbortError') { process.stdout.write(`\n ${C.faint('⊘ interrupted')}\n\n`); continue }
|
|
261
|
+
process.stdout.write('\n')
|
|
262
|
+
if (e instanceof BrainConfigError) error(e.message)
|
|
263
|
+
else error(String(e instanceof Error ? e.message : e))
|
|
264
|
+
}
|
|
265
|
+
aborter = null
|
|
266
|
+
}
|
|
267
|
+
await cleanup()
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
program
|
|
271
|
+
.command('ask [prompt...]')
|
|
272
|
+
.description('One-shot Dragon agent question (read-only unless --auto; pipeable)')
|
|
273
|
+
.option('--brain <provider>', 'reasoning brain: claude | worker | local | openai | ghost')
|
|
274
|
+
.option('--model <id>', 'override the model id')
|
|
275
|
+
.option('--no-wyrm', 'disable Wyrm memory')
|
|
276
|
+
.option('--no-portal', "don't expose the hosted account assistant")
|
|
277
|
+
.option('--auto', 'allow file writes + shell (off = read-only)', false)
|
|
278
|
+
.option('--plan', 'read-only (deny all writes/shell)', false)
|
|
279
|
+
.option('--sovereign', 'local brain + Wyrm only — nothing leaves the host', false)
|
|
280
|
+
.option('--sandbox', 'run shell in a bwrap jail', false)
|
|
281
|
+
.option('--no-trace', 'do not record a trace for DragonSpark training')
|
|
282
|
+
.option('--cwd <dir>', 'working directory')
|
|
283
|
+
.action(async (parts: string[] = [], opts: SetupOpts & { auto?: boolean; plan?: boolean; sandbox?: boolean; trace?: boolean }) => {
|
|
284
|
+
let prompt = parts.join(' ').trim()
|
|
285
|
+
if (prompt === '-' || (!prompt && !stdin.isTTY)) prompt = (await readStdinAll()).trim()
|
|
286
|
+
if (!prompt) { error('no prompt. Pass args, pipe stdin, or use "-".'); process.exit(1) }
|
|
287
|
+
|
|
288
|
+
const s = await setup(opts, false)
|
|
289
|
+
if (!s) { process.exit(1) }
|
|
290
|
+
|
|
291
|
+
const denied: string[] = []
|
|
292
|
+
const toolCtx: ToolContext = {
|
|
293
|
+
cwd: s!.cwd,
|
|
294
|
+
sandbox: !!opts.sandbox,
|
|
295
|
+
approve: async (summary, o) => { if (opts.auto && !opts.plan && !o?.dangerous) return true; denied.push(summary); return false },
|
|
296
|
+
}
|
|
297
|
+
const messages: BrainMessage[] = []
|
|
298
|
+
const deps: AgentDeps = { ...s!, toolCtx, messages }
|
|
299
|
+
const render: AgentRender = {
|
|
300
|
+
onAssistantStart() {},
|
|
301
|
+
onDelta(t) { process.stdout.write(t) },
|
|
302
|
+
onToolStart(summary) { process.stderr.write(C.faint(`⚙ ${summary}\n`)) },
|
|
303
|
+
onToolEnd(_s, preview, ok) { process.stderr.write(C.faint(` ${ok ? '✓' : '✗'} ${preview}\n`)) },
|
|
304
|
+
}
|
|
305
|
+
try {
|
|
306
|
+
await runAgent(deps, expandFileRefs(prompt), render, new AbortController().signal)
|
|
307
|
+
process.stdout.write('\n')
|
|
308
|
+
if (traceEnabled(opts.trace === false)) recordTurn(messages, { cwd: s!.cwd, brain: `${s!.brain.id}:${s!.brain.model}`, context: s!.primed })
|
|
309
|
+
if (denied.length) process.stderr.write(C.faint(`\n(${denied.length} mutating action(s) skipped — re-run with --auto to allow)\n`))
|
|
310
|
+
} catch (e) {
|
|
311
|
+
if (e instanceof BrainConfigError) error(e.message)
|
|
312
|
+
else error(String(e instanceof Error ? e.message : e))
|
|
313
|
+
await s!.wyrm?.close(); await s!.mcp?.close()
|
|
314
|
+
process.exit(1)
|
|
315
|
+
}
|
|
316
|
+
await s!.wyrm?.close()
|
|
317
|
+
})
|
|
318
|
+
}
|