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
package/dist/agent.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client for the Dragon assistant — account.ghosts.lk `POST /api/v1/agent`.
|
|
3
|
+
*
|
|
4
|
+
* The endpoint runs a server-side tool-calling loop, then streams the answer as
|
|
5
|
+
* SSE. Two frame kinds arrive on the `data:` channel:
|
|
6
|
+
* - `{"tools":[{"name","ok"}]}` — once, up front, if tools ran (→ onTools)
|
|
7
|
+
* - `{"response":"…"}` — token deltas of the natural-language answer
|
|
8
|
+
* - `[DONE]` — terminator
|
|
9
|
+
*
|
|
10
|
+
* Tools execute server-side and role-gated; the client only DISPLAYS which ran.
|
|
11
|
+
* Request body is `{ messages:[{role,content}], surface }` — the server keeps
|
|
12
|
+
* only user/assistant turns and prepends its own system prompt + memory.
|
|
13
|
+
*
|
|
14
|
+
* Copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
|
|
15
|
+
*/
|
|
16
|
+
export interface AgentMsg {
|
|
17
|
+
role: 'user' | 'assistant';
|
|
18
|
+
content: string;
|
|
19
|
+
}
|
|
20
|
+
export interface ToolChip {
|
|
21
|
+
name: string;
|
|
22
|
+
ok: boolean;
|
|
23
|
+
}
|
|
24
|
+
/** Surfaces the server understands — they scope the tool set + system context. */
|
|
25
|
+
export declare const SURFACES: readonly ["dashboard", "admin", "chat", "activity", "marketing"];
|
|
26
|
+
export type Surface = (typeof SURFACES)[number];
|
|
27
|
+
export type AgentErrorKind = 'unauthenticated' | 'quota' | 'unavailable' | 'http' | 'network';
|
|
28
|
+
export declare class AgentError extends Error {
|
|
29
|
+
kind: AgentErrorKind;
|
|
30
|
+
detail?: unknown | undefined;
|
|
31
|
+
constructor(kind: AgentErrorKind, message: string, detail?: unknown | undefined);
|
|
32
|
+
}
|
|
33
|
+
export interface StreamAgentOpts {
|
|
34
|
+
messages: AgentMsg[];
|
|
35
|
+
surface: string;
|
|
36
|
+
onDelta: (s: string) => void;
|
|
37
|
+
onTools?: (tools: ToolChip[]) => void;
|
|
38
|
+
signal?: AbortSignal;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Stream one assistant turn. Emits tool chips (if any) then token deltas, and
|
|
42
|
+
* returns the assembled answer when `[DONE]` arrives. Throws AgentError on
|
|
43
|
+
* 401 (unauthenticated), 429 (quota), 502 (model down), or transport failure.
|
|
44
|
+
*/
|
|
45
|
+
export declare function streamAgent(opts: StreamAgentOpts): Promise<string>;
|
|
46
|
+
//# sourceMappingURL=agent.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,MAAM,WAAW,QAAQ;IAAG,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE;AACzE,MAAM,WAAW,QAAQ;IAAG,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,OAAO,CAAA;CAAE;AAEvD,kFAAkF;AAClF,eAAO,MAAM,QAAQ,kEAAmE,CAAA;AACxF,MAAM,MAAM,OAAO,GAAG,CAAC,OAAO,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAA;AAE/C,MAAM,MAAM,cAAc,GAAG,iBAAiB,GAAG,OAAO,GAAG,aAAa,GAAG,MAAM,GAAG,SAAS,CAAA;AAE7F,qBAAa,UAAW,SAAQ,KAAK;IAChB,IAAI,EAAE,cAAc;IAA0B,MAAM,CAAC,EAAE,OAAO;gBAA9D,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,EAAS,MAAM,CAAC,EAAE,OAAO,YAAA;CAIlF;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,QAAQ,EAAE,CAAA;IACpB,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;IAC5B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,IAAI,CAAA;IACrC,MAAM,CAAC,EAAE,WAAW,CAAA;CACrB;AAED;;;;GAIG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CAkExE"}
|
package/dist/agent.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client for the Dragon assistant — account.ghosts.lk `POST /api/v1/agent`.
|
|
3
|
+
*
|
|
4
|
+
* The endpoint runs a server-side tool-calling loop, then streams the answer as
|
|
5
|
+
* SSE. Two frame kinds arrive on the `data:` channel:
|
|
6
|
+
* - `{"tools":[{"name","ok"}]}` — once, up front, if tools ran (→ onTools)
|
|
7
|
+
* - `{"response":"…"}` — token deltas of the natural-language answer
|
|
8
|
+
* - `[DONE]` — terminator
|
|
9
|
+
*
|
|
10
|
+
* Tools execute server-side and role-gated; the client only DISPLAYS which ran.
|
|
11
|
+
* Request body is `{ messages:[{role,content}], surface }` — the server keeps
|
|
12
|
+
* only user/assistant turns and prepends its own system prompt + memory.
|
|
13
|
+
*
|
|
14
|
+
* Copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
|
|
15
|
+
*/
|
|
16
|
+
import { resolveAuth } from './auth.js';
|
|
17
|
+
/** Surfaces the server understands — they scope the tool set + system context. */
|
|
18
|
+
export const SURFACES = ['dashboard', 'admin', 'chat', 'activity', 'marketing'];
|
|
19
|
+
export class AgentError extends Error {
|
|
20
|
+
kind;
|
|
21
|
+
detail;
|
|
22
|
+
constructor(kind, message, detail) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.kind = kind;
|
|
25
|
+
this.detail = detail;
|
|
26
|
+
this.name = 'AgentError';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Stream one assistant turn. Emits tool chips (if any) then token deltas, and
|
|
31
|
+
* returns the assembled answer when `[DONE]` arrives. Throws AgentError on
|
|
32
|
+
* 401 (unauthenticated), 429 (quota), 502 (model down), or transport failure.
|
|
33
|
+
*/
|
|
34
|
+
export async function streamAgent(opts) {
|
|
35
|
+
const { apiBase, headers, mode } = resolveAuth();
|
|
36
|
+
let res;
|
|
37
|
+
try {
|
|
38
|
+
res = await fetch(`${apiBase}/api/v1/agent`, {
|
|
39
|
+
method: 'POST',
|
|
40
|
+
headers: { 'content-type': 'application/json', ...headers },
|
|
41
|
+
body: JSON.stringify({ messages: opts.messages, surface: opts.surface }),
|
|
42
|
+
signal: opts.signal,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
if (e?.name === 'AbortError')
|
|
47
|
+
throw e;
|
|
48
|
+
throw new AgentError('network', `cannot reach ${apiBase}: ${String(e instanceof Error ? e.message : e)}`);
|
|
49
|
+
}
|
|
50
|
+
if (res.status === 401) {
|
|
51
|
+
const b = (await res.json().catch(() => ({})));
|
|
52
|
+
const hint = mode === 'none'
|
|
53
|
+
? 'no credentials configured — run `dragon login`'
|
|
54
|
+
: 'your session is invalid or expired — run `dragon login` again';
|
|
55
|
+
throw new AgentError('unauthenticated', hint, b);
|
|
56
|
+
}
|
|
57
|
+
if (res.status === 429) {
|
|
58
|
+
const b = (await res.json().catch(() => ({})));
|
|
59
|
+
const reset = b.reset_at ? new Date(b.reset_at).toLocaleString() : '—';
|
|
60
|
+
throw new AgentError('quota', `daily quota reached (${b.used ?? '?'} / ${b.cap ?? '?'} tokens). Resets ${reset}.`, b);
|
|
61
|
+
}
|
|
62
|
+
if (res.status === 502) {
|
|
63
|
+
throw new AgentError('unavailable', 'the model is momentarily unavailable — try again in a moment.');
|
|
64
|
+
}
|
|
65
|
+
if (!res.ok || !res.body) {
|
|
66
|
+
const t = await res.text().catch(() => res.statusText);
|
|
67
|
+
throw new AgentError('http', `HTTP ${res.status}: ${t}`);
|
|
68
|
+
}
|
|
69
|
+
const reader = res.body.getReader();
|
|
70
|
+
const decoder = new TextDecoder();
|
|
71
|
+
let buffer = '';
|
|
72
|
+
let assembled = '';
|
|
73
|
+
for (;;) {
|
|
74
|
+
const { value, done } = await reader.read();
|
|
75
|
+
if (done)
|
|
76
|
+
break;
|
|
77
|
+
buffer += decoder.decode(value, { stream: true });
|
|
78
|
+
const lines = buffer.split('\n');
|
|
79
|
+
buffer = lines.pop() ?? ''; // hold the partial trailing line
|
|
80
|
+
for (const line of lines) {
|
|
81
|
+
if (!line.startsWith('data: '))
|
|
82
|
+
continue;
|
|
83
|
+
const data = line.slice(6);
|
|
84
|
+
if (data === '[DONE]')
|
|
85
|
+
return assembled;
|
|
86
|
+
try {
|
|
87
|
+
const obj = JSON.parse(data);
|
|
88
|
+
if (Array.isArray(obj.tools)) {
|
|
89
|
+
opts.onTools?.(obj.tools.map((t) => ({ name: String(t.name), ok: t.ok !== false })));
|
|
90
|
+
}
|
|
91
|
+
else if (typeof obj.response === 'string' && obj.response) {
|
|
92
|
+
assembled += obj.response;
|
|
93
|
+
opts.onDelta(obj.response);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
/* skip a malformed frame */
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return assembled;
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=agent.js.map
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth for the Dragon assistant (account.ghosts.lk /api/v1/agent).
|
|
3
|
+
*
|
|
4
|
+
* The assistant is account-scoped: it answers AS the signed-in user and gates
|
|
5
|
+
* its tools by role. The browser authenticates with the `gp_session` HttpOnly
|
|
6
|
+
* cookie minted by Google/GitHub OAuth. A CLI has no browser, so we support two
|
|
7
|
+
* credential carriers, in priority order:
|
|
8
|
+
*
|
|
9
|
+
* 1. Bearer token — `DRAGON_TOKEN` env or `auth.token` in config. The primary
|
|
10
|
+
* path: a 90-day personal access token (`dgn_…`) minted by `dragon login`'s
|
|
11
|
+
* browser device-code flow. Backend is live on account.ghosts.lk.
|
|
12
|
+
* 2. Session cookie — `DRAGON_SESSION` env or `auth.session` in config. The
|
|
13
|
+
* `gp_session` value copied from a signed-in browser. The `--paste` fallback
|
|
14
|
+
* for headless boxes where the device flow can't open a browser.
|
|
15
|
+
*
|
|
16
|
+
* Resolution is env-first so a shell can override the stored credential without
|
|
17
|
+
* touching the config file.
|
|
18
|
+
*
|
|
19
|
+
* Copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
|
|
20
|
+
*/
|
|
21
|
+
import { type DragonConfig } from './config.js';
|
|
22
|
+
export declare const DEFAULT_API = "https://account.ghosts.lk";
|
|
23
|
+
/** True if `raw` is a URL a browser will actually open: http(s), a sane port, and
|
|
24
|
+
* https for any non-loopback host (no plaintext auth over the network). */
|
|
25
|
+
export declare function isBrowsableHttpUrl(raw: string): boolean;
|
|
26
|
+
/** A trustworthy assistant origin, self-healing: a malformed/restricted-port value
|
|
27
|
+
* (e.g. a stale config) silently falls back to the default instead of breaking login. */
|
|
28
|
+
export declare function sanitizeApiBase(raw?: string): string;
|
|
29
|
+
export type AuthMode = 'token' | 'session' | 'none';
|
|
30
|
+
export interface ResolvedAuth {
|
|
31
|
+
apiBase: string;
|
|
32
|
+
headers: Record<string, string>;
|
|
33
|
+
mode: AuthMode;
|
|
34
|
+
email?: string;
|
|
35
|
+
}
|
|
36
|
+
/** Credential values become HTTP header content — accept only printable, space-free
|
|
37
|
+
* ASCII so a CR/LF or control char can't smuggle extra headers. */
|
|
38
|
+
export declare function validCred(v?: string): string | undefined;
|
|
39
|
+
/** The credential-bearing env vars auth reads — extracted so the precedence
|
|
40
|
+
* logic is unit-testable without a real shell, config file, or second machine. */
|
|
41
|
+
export interface AuthEnv {
|
|
42
|
+
DRAGON_API?: string;
|
|
43
|
+
DRAGON_TOKEN?: string;
|
|
44
|
+
DRAGON_SESSION?: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Pure auth resolution: env-first, then config, behind the cred-injection
|
|
48
|
+
* (`validCred`) and restricted-origin (`sanitizeApiBase`) guards. Token beats
|
|
49
|
+
* session beats none. `resolveAuth()` is just this fed the real env + config —
|
|
50
|
+
* keeping the decision logic deterministic and machine-independent for tests.
|
|
51
|
+
*/
|
|
52
|
+
export declare function resolveAuthFrom(env: AuthEnv, cfg: DragonConfig): ResolvedAuth;
|
|
53
|
+
/** Resolve the assistant origin + credential headers from env then config. */
|
|
54
|
+
export declare function resolveAuth(): ResolvedAuth;
|
|
55
|
+
/** Merge a patch into config.auth and persist. */
|
|
56
|
+
export declare function saveAuth(patch: Partial<NonNullable<DragonConfig['auth']>>): void;
|
|
57
|
+
/** Forget all stored credentials (keeps a custom apiBase if set). */
|
|
58
|
+
export declare function clearAuth(): void;
|
|
59
|
+
export interface WhoAmI {
|
|
60
|
+
ok: boolean;
|
|
61
|
+
status: number;
|
|
62
|
+
email?: string;
|
|
63
|
+
error?: string;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Validate the current (or a supplied) credential against the cheap authed
|
|
67
|
+
* endpoint `GET /api/v1/me/licenses` — no LLM cost. Returns the identity email
|
|
68
|
+
* on success so the caller can confirm + store who we signed in as.
|
|
69
|
+
*/
|
|
70
|
+
export declare function whoami(override?: {
|
|
71
|
+
headers?: Record<string, string>;
|
|
72
|
+
apiBase?: string;
|
|
73
|
+
}): Promise<WhoAmI>;
|
|
74
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAA0B,KAAK,YAAY,EAAE,MAAM,aAAa,CAAA;AAEvE,eAAO,MAAM,WAAW,8BAA8B,CAAA;AAYtD;4EAC4E;AAC5E,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAQvD;AAED;0FAC0F;AAC1F,wBAAgB,eAAe,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAGpD;AAED,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAA;AAEnD,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,IAAI,EAAE,QAAQ,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED;oEACoE;AACpE,wBAAgB,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAExD;AAED;mFACmF;AACnF,MAAM,WAAW,OAAO;IACtB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,YAAY,GAAG,YAAY,CAQ7E;AAED,8EAA8E;AAC9E,wBAAgB,WAAW,IAAI,YAAY,CAE1C;AAED,kDAAkD;AAClD,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,WAAW,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,IAAI,CAIhF;AAED,qEAAqE;AACrE,wBAAgB,SAAS,IAAI,IAAI,CAKhC;AAED,MAAM,WAAW,MAAM;IAAG,EAAE,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE;AAEvF;;;;GAIG;AACH,wBAAsB,MAAM,CAAC,QAAQ,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAY/G"}
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth for the Dragon assistant (account.ghosts.lk /api/v1/agent).
|
|
3
|
+
*
|
|
4
|
+
* The assistant is account-scoped: it answers AS the signed-in user and gates
|
|
5
|
+
* its tools by role. The browser authenticates with the `gp_session` HttpOnly
|
|
6
|
+
* cookie minted by Google/GitHub OAuth. A CLI has no browser, so we support two
|
|
7
|
+
* credential carriers, in priority order:
|
|
8
|
+
*
|
|
9
|
+
* 1. Bearer token — `DRAGON_TOKEN` env or `auth.token` in config. The primary
|
|
10
|
+
* path: a 90-day personal access token (`dgn_…`) minted by `dragon login`'s
|
|
11
|
+
* browser device-code flow. Backend is live on account.ghosts.lk.
|
|
12
|
+
* 2. Session cookie — `DRAGON_SESSION` env or `auth.session` in config. The
|
|
13
|
+
* `gp_session` value copied from a signed-in browser. The `--paste` fallback
|
|
14
|
+
* for headless boxes where the device flow can't open a browser.
|
|
15
|
+
*
|
|
16
|
+
* Resolution is env-first so a shell can override the stored credential without
|
|
17
|
+
* touching the config file.
|
|
18
|
+
*
|
|
19
|
+
* Copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
|
|
20
|
+
*/
|
|
21
|
+
import { loadConfig, saveConfig } from './config.js';
|
|
22
|
+
export const DEFAULT_API = 'https://account.ghosts.lk';
|
|
23
|
+
// Ports browsers refuse to open (ERR_UNSAFE_PORT / "this address is restricted").
|
|
24
|
+
// We guard against ever handing one of these to a browser or using it as an origin.
|
|
25
|
+
const RESTRICTED_PORTS = new Set([
|
|
26
|
+
1, 7, 9, 11, 13, 15, 17, 19, 20, 21, 22, 23, 25, 37, 42, 43, 53, 69, 77, 79, 87, 95,
|
|
27
|
+
101, 102, 103, 104, 109, 110, 111, 113, 115, 117, 119, 123, 135, 137, 139, 143, 161,
|
|
28
|
+
179, 389, 427, 465, 512, 513, 514, 515, 526, 530, 531, 532, 540, 548, 554, 556, 563,
|
|
29
|
+
587, 601, 636, 989, 990, 993, 995, 1719, 1720, 1723, 2049, 3659, 4045, 5060, 5061,
|
|
30
|
+
6000, 6566, 6665, 6666, 6667, 6668, 6669, 6697, 10080,
|
|
31
|
+
]);
|
|
32
|
+
/** True if `raw` is a URL a browser will actually open: http(s), a sane port, and
|
|
33
|
+
* https for any non-loopback host (no plaintext auth over the network). */
|
|
34
|
+
export function isBrowsableHttpUrl(raw) {
|
|
35
|
+
let u;
|
|
36
|
+
try {
|
|
37
|
+
u = new URL(raw);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
if (u.protocol !== 'http:' && u.protocol !== 'https:')
|
|
43
|
+
return false;
|
|
44
|
+
const isLocal = u.hostname === 'localhost' || u.hostname === '127.0.0.1' || u.hostname === '::1';
|
|
45
|
+
if (!isLocal && u.protocol !== 'https:')
|
|
46
|
+
return false;
|
|
47
|
+
const port = u.port ? Number(u.port) : u.protocol === 'https:' ? 443 : 80;
|
|
48
|
+
return !RESTRICTED_PORTS.has(port);
|
|
49
|
+
}
|
|
50
|
+
/** A trustworthy assistant origin, self-healing: a malformed/restricted-port value
|
|
51
|
+
* (e.g. a stale config) silently falls back to the default instead of breaking login. */
|
|
52
|
+
export function sanitizeApiBase(raw) {
|
|
53
|
+
const trimmed = (raw ?? '').replace(/\/+$/, '');
|
|
54
|
+
return trimmed && isBrowsableHttpUrl(trimmed) ? trimmed : DEFAULT_API;
|
|
55
|
+
}
|
|
56
|
+
/** Credential values become HTTP header content — accept only printable, space-free
|
|
57
|
+
* ASCII so a CR/LF or control char can't smuggle extra headers. */
|
|
58
|
+
export function validCred(v) {
|
|
59
|
+
return v && /^[\x21-\x7e]+$/.test(v) ? v : undefined;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Pure auth resolution: env-first, then config, behind the cred-injection
|
|
63
|
+
* (`validCred`) and restricted-origin (`sanitizeApiBase`) guards. Token beats
|
|
64
|
+
* session beats none. `resolveAuth()` is just this fed the real env + config —
|
|
65
|
+
* keeping the decision logic deterministic and machine-independent for tests.
|
|
66
|
+
*/
|
|
67
|
+
export function resolveAuthFrom(env, cfg) {
|
|
68
|
+
const apiBase = sanitizeApiBase(env.DRAGON_API || cfg.auth?.apiBase);
|
|
69
|
+
const token = validCred(env.DRAGON_TOKEN || cfg.auth?.token);
|
|
70
|
+
const session = validCred(env.DRAGON_SESSION || cfg.auth?.session);
|
|
71
|
+
const email = cfg.auth?.email;
|
|
72
|
+
if (token)
|
|
73
|
+
return { apiBase, headers: { authorization: `Bearer ${token}` }, mode: 'token', email };
|
|
74
|
+
if (session)
|
|
75
|
+
return { apiBase, headers: { cookie: `gp_session=${session}` }, mode: 'session', email };
|
|
76
|
+
return { apiBase, headers: {}, mode: 'none', email };
|
|
77
|
+
}
|
|
78
|
+
/** Resolve the assistant origin + credential headers from env then config. */
|
|
79
|
+
export function resolveAuth() {
|
|
80
|
+
return resolveAuthFrom(process.env, loadConfig());
|
|
81
|
+
}
|
|
82
|
+
/** Merge a patch into config.auth and persist. */
|
|
83
|
+
export function saveAuth(patch) {
|
|
84
|
+
const cfg = loadConfig();
|
|
85
|
+
cfg.auth = { ...(cfg.auth ?? {}), ...patch };
|
|
86
|
+
saveConfig(cfg);
|
|
87
|
+
}
|
|
88
|
+
/** Forget all stored credentials (keeps a custom apiBase if set). */
|
|
89
|
+
export function clearAuth() {
|
|
90
|
+
const cfg = loadConfig();
|
|
91
|
+
const apiBase = cfg.auth?.apiBase;
|
|
92
|
+
cfg.auth = apiBase ? { apiBase } : {};
|
|
93
|
+
saveConfig(cfg);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Validate the current (or a supplied) credential against the cheap authed
|
|
97
|
+
* endpoint `GET /api/v1/me/licenses` — no LLM cost. Returns the identity email
|
|
98
|
+
* on success so the caller can confirm + store who we signed in as.
|
|
99
|
+
*/
|
|
100
|
+
export async function whoami(override) {
|
|
101
|
+
const base = override?.apiBase ?? resolveAuth().apiBase;
|
|
102
|
+
const headers = override?.headers ?? resolveAuth().headers;
|
|
103
|
+
try {
|
|
104
|
+
const res = await fetch(`${base}/api/v1/me/licenses`, { headers });
|
|
105
|
+
if (res.status === 401)
|
|
106
|
+
return { ok: false, status: 401, error: 'not signed in' };
|
|
107
|
+
if (!res.ok)
|
|
108
|
+
return { ok: false, status: res.status, error: `HTTP ${res.status}` };
|
|
109
|
+
const body = (await res.json().catch(() => ({})));
|
|
110
|
+
return { ok: true, status: 200, email: body.account?.email };
|
|
111
|
+
}
|
|
112
|
+
catch (e) {
|
|
113
|
+
return { ok: false, status: 0, error: String(e instanceof Error ? e.message : e) };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=auth.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude brain — Anthropic Messages API via the official SDK. The default,
|
|
3
|
+
* "amazing at programming" path.
|
|
4
|
+
*
|
|
5
|
+
* Maps our normalized BrainMessage[] onto Anthropic's content-block format:
|
|
6
|
+
* - assistant tool calls → `tool_use` blocks
|
|
7
|
+
* - tool results → `tool_result` blocks (folded into a user message;
|
|
8
|
+
* consecutive tool messages are batched into one user turn, as the API wants)
|
|
9
|
+
* Streams `text` deltas and collects `tool_use` blocks from the final message.
|
|
10
|
+
*
|
|
11
|
+
* Copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
|
|
12
|
+
*/
|
|
13
|
+
import type { Brain } from './types.js';
|
|
14
|
+
export declare const DEFAULT_CLAUDE_MODEL = "claude-sonnet-4-6";
|
|
15
|
+
export declare function makeClaudeBrain(opts: {
|
|
16
|
+
apiKey: string;
|
|
17
|
+
model?: string;
|
|
18
|
+
}): Brain;
|
|
19
|
+
//# sourceMappingURL=anthropic.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anthropic.d.ts","sourceRoot":"","sources":["../../src/brain/anthropic.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,KAAK,EAA+C,MAAM,YAAY,CAAA;AAEpF,eAAO,MAAM,oBAAoB,sBAAsB,CAAA;AAqCvD,wBAAgB,eAAe,CAAC,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,KAAK,CA6B/E"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude brain — Anthropic Messages API via the official SDK. The default,
|
|
3
|
+
* "amazing at programming" path.
|
|
4
|
+
*
|
|
5
|
+
* Maps our normalized BrainMessage[] onto Anthropic's content-block format:
|
|
6
|
+
* - assistant tool calls → `tool_use` blocks
|
|
7
|
+
* - tool results → `tool_result` blocks (folded into a user message;
|
|
8
|
+
* consecutive tool messages are batched into one user turn, as the API wants)
|
|
9
|
+
* Streams `text` deltas and collects `tool_use` blocks from the final message.
|
|
10
|
+
*
|
|
11
|
+
* Copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
|
|
12
|
+
*/
|
|
13
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
14
|
+
export const DEFAULT_CLAUDE_MODEL = 'claude-sonnet-4-6';
|
|
15
|
+
/** Fold our flat BrainMessage[] into Anthropic's role-batched content blocks. */
|
|
16
|
+
function toAnthropic(messages) {
|
|
17
|
+
const out = [];
|
|
18
|
+
for (const m of messages) {
|
|
19
|
+
if (m.role === 'tool') {
|
|
20
|
+
const block = { type: 'tool_result', tool_use_id: m.toolCallId ?? '', content: m.content };
|
|
21
|
+
const last = out[out.length - 1];
|
|
22
|
+
if (last && last.role === 'user' && last.content.every((b) => b.type === 'tool_result'))
|
|
23
|
+
last.content.push(block);
|
|
24
|
+
else
|
|
25
|
+
out.push({ role: 'user', content: [block] });
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (m.role === 'assistant') {
|
|
29
|
+
const blocks = [];
|
|
30
|
+
if (m.content)
|
|
31
|
+
blocks.push({ type: 'text', text: m.content });
|
|
32
|
+
for (const tc of m.toolCalls ?? [])
|
|
33
|
+
blocks.push({ type: 'tool_use', id: tc.id, name: tc.name, input: tc.arguments });
|
|
34
|
+
out.push({ role: 'assistant', content: blocks.length ? blocks : [{ type: 'text', text: '' }] });
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
// user
|
|
38
|
+
out.push({ role: 'user', content: [{ type: 'text', text: m.content }] });
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
function toAnthropicTools(tools) {
|
|
43
|
+
return tools.map((t) => ({ name: t.name, description: t.description, input_schema: t.parameters }));
|
|
44
|
+
}
|
|
45
|
+
export function makeClaudeBrain(opts) {
|
|
46
|
+
const client = new Anthropic({ apiKey: opts.apiKey });
|
|
47
|
+
const model = opts.model || DEFAULT_CLAUDE_MODEL;
|
|
48
|
+
return {
|
|
49
|
+
id: 'claude',
|
|
50
|
+
model,
|
|
51
|
+
async turn(t) {
|
|
52
|
+
const stream = client.messages.stream({
|
|
53
|
+
model,
|
|
54
|
+
max_tokens: t.maxTokens ?? 8192,
|
|
55
|
+
system: t.system,
|
|
56
|
+
messages: toAnthropic(t.messages),
|
|
57
|
+
tools: t.tools.length ? toAnthropicTools(t.tools) : undefined,
|
|
58
|
+
}, { signal: t.signal });
|
|
59
|
+
if (t.onDelta)
|
|
60
|
+
stream.on('text', (delta) => t.onDelta(delta));
|
|
61
|
+
const final = await stream.finalMessage();
|
|
62
|
+
let text = '';
|
|
63
|
+
const toolCalls = [];
|
|
64
|
+
for (const block of final.content) {
|
|
65
|
+
if (block.type === 'text')
|
|
66
|
+
text += block.text;
|
|
67
|
+
else if (block.type === 'tool_use')
|
|
68
|
+
toolCalls.push({ id: block.id, name: block.name, arguments: (block.input ?? {}) });
|
|
69
|
+
}
|
|
70
|
+
return { text, toolCalls };
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=anthropic.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude-CLI brain — drive the agent with the LOCAL `claude` CLI (Claude Code) in
|
|
3
|
+
* headless mode, reusing the operator's EXISTING Claude Code auth. No separate
|
|
4
|
+
* ANTHROPIC_API_KEY needed: if `--brain claude` has no API key but the `claude` binary
|
|
5
|
+
* is on PATH, the factory falls back here ("use this Claude").
|
|
6
|
+
*
|
|
7
|
+
* It runs `claude -p --output-format json` per turn, feeding the rendered conversation
|
|
8
|
+
* on stdin and asking for ONLY the next tool call(s) as a JSON array (EMBER's exact
|
|
9
|
+
* format) — so the same parser handles it AND the captured ~/.dragon/traces are
|
|
10
|
+
* Claude-quality decisions IN dragon-cli's contract: the gold distillation data for the
|
|
11
|
+
* next EMBER. Caveats: heavier than the API (spawns Claude Code per turn) and it bills
|
|
12
|
+
* the operator's Claude Code usage — fine for flywheel-filling, not low-latency chat.
|
|
13
|
+
*
|
|
14
|
+
* Copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
|
|
15
|
+
*/
|
|
16
|
+
import type { Brain } from './types.js';
|
|
17
|
+
export declare function makeClaudeCliBrain(opts?: {
|
|
18
|
+
model?: string;
|
|
19
|
+
}): Brain;
|
|
20
|
+
//# sourceMappingURL=claude-cli.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claude-cli.d.ts","sourceRoot":"","sources":["../../src/brain/claude-cli.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,KAAK,EAAE,KAAK,EAAqC,MAAM,YAAY,CAAA;AAwC1E,wBAAgB,kBAAkB,CAAC,IAAI,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,KAAK,CAoBnE"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude-CLI brain — drive the agent with the LOCAL `claude` CLI (Claude Code) in
|
|
3
|
+
* headless mode, reusing the operator's EXISTING Claude Code auth. No separate
|
|
4
|
+
* ANTHROPIC_API_KEY needed: if `--brain claude` has no API key but the `claude` binary
|
|
5
|
+
* is on PATH, the factory falls back here ("use this Claude").
|
|
6
|
+
*
|
|
7
|
+
* It runs `claude -p --output-format json` per turn, feeding the rendered conversation
|
|
8
|
+
* on stdin and asking for ONLY the next tool call(s) as a JSON array (EMBER's exact
|
|
9
|
+
* format) — so the same parser handles it AND the captured ~/.dragon/traces are
|
|
10
|
+
* Claude-quality decisions IN dragon-cli's contract: the gold distillation data for the
|
|
11
|
+
* next EMBER. Caveats: heavier than the API (spawns Claude Code per turn) and it bills
|
|
12
|
+
* the operator's Claude Code usage — fine for flywheel-filling, not low-latency chat.
|
|
13
|
+
*
|
|
14
|
+
* Copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
|
|
15
|
+
*/
|
|
16
|
+
import { spawn } from 'node:child_process';
|
|
17
|
+
import { parseEmberToolCalls } from './ghost-ember.js';
|
|
18
|
+
function render(system, messages, toolNames) {
|
|
19
|
+
const convo = messages.map((m) => {
|
|
20
|
+
if (m.role === 'tool')
|
|
21
|
+
return `[tool result: ${m.toolName ?? 'tool'}]\n${m.content}`.slice(0, 4000);
|
|
22
|
+
if (m.role === 'assistant')
|
|
23
|
+
return `[assistant] ${m.toolCalls?.length ? JSON.stringify(m.toolCalls.map((t) => ({ tool: t.name, arguments: t.arguments }))) : m.content}`;
|
|
24
|
+
return `[user] ${m.content}`;
|
|
25
|
+
}).join('\n\n');
|
|
26
|
+
return `${system}\n\n── CONVERSATION SO FAR ──\n${convo}\n\n── YOUR TASK ──\n` +
|
|
27
|
+
'You are the REASONING brain of an external agent — do NOT use your own tools or take any action yourself. ' +
|
|
28
|
+
'When you need the agent to act, respond with ONLY a JSON array of the next tool call(s), no prose:\n' +
|
|
29
|
+
'[{"tool":"<name>","arguments":{…}}]\n' +
|
|
30
|
+
'When you have enough to finish, respond with your final answer as PLAIN TEXT (no JSON array). ' +
|
|
31
|
+
`The agent's available tools are: ${toolNames.join(', ')}.`;
|
|
32
|
+
}
|
|
33
|
+
function runClaude(args, prompt, signal) {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const cp = spawn('claude', args, { stdio: ['pipe', 'pipe', 'ignore'], signal });
|
|
36
|
+
let out = '';
|
|
37
|
+
cp.stdout.on('data', (d) => { out += d; });
|
|
38
|
+
cp.on('error', reject);
|
|
39
|
+
cp.on('close', () => {
|
|
40
|
+
// --output-format json → either a single result object or a stream array;
|
|
41
|
+
// the assistant text lives in the `result` field of the type:'result' event.
|
|
42
|
+
try {
|
|
43
|
+
const parsed = JSON.parse(out);
|
|
44
|
+
if (Array.isArray(parsed)) {
|
|
45
|
+
const res = parsed.find((e) => e && e.type === 'result');
|
|
46
|
+
return resolve(typeof res?.result === 'string' ? res.result : '');
|
|
47
|
+
}
|
|
48
|
+
return resolve(typeof parsed?.result === 'string' ? parsed.result : out);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
resolve(out);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
cp.stdin.write(prompt);
|
|
55
|
+
cp.stdin.end();
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
export function makeClaudeCliBrain(opts) {
|
|
59
|
+
return {
|
|
60
|
+
id: 'claude',
|
|
61
|
+
model: opts?.model ? `${opts.model} (cli)` : 'claude-code (cli)',
|
|
62
|
+
async turn(t) {
|
|
63
|
+
// --model FIRST (so the variadic --disallowedTools below doesn't swallow it).
|
|
64
|
+
const args = ['-p', '--output-format', 'json'];
|
|
65
|
+
if (opts?.model)
|
|
66
|
+
args.push('--model', opts.model);
|
|
67
|
+
// Disable Claude Code's OWN acting tools so it can't do the work itself — it must
|
|
68
|
+
// route through dragon-cli (its decisions become EMBER's training traces) and only
|
|
69
|
+
// produce prose once dragon-cli feeds the results back. Variadic → keep LAST.
|
|
70
|
+
args.push('--disallowedTools', 'Bash', 'Edit', 'Write', 'Read', 'Grep', 'Glob', 'MultiEdit', 'NotebookEdit', 'Task', 'WebFetch', 'WebSearch');
|
|
71
|
+
const prompt = render(t.system, t.messages, t.tools.map((x) => x.name));
|
|
72
|
+
const text = await runClaude(args, prompt, t.signal);
|
|
73
|
+
const toolCalls = parseEmberToolCalls(text);
|
|
74
|
+
t.onDelta?.(toolCalls.length ? toolCalls.map((c) => `→ ${c.name}`).join(' ') : text);
|
|
75
|
+
return { text: toolCalls.length ? '' : text, toolCalls };
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=claude-cli.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ghost / EMBER brain — Ghost Protocol's own local model (DragonSpark's EMBER,
|
|
3
|
+
* served by Ollama) as the agent's reasoning layer.
|
|
4
|
+
*
|
|
5
|
+
* EMBER is a small specialist FINE-TUNED to emit tool calls as a JSON array in the
|
|
6
|
+
* response TEXT — `[{"tool":"Bash","arguments":{…}}]` — NOT via the OpenAI native
|
|
7
|
+
* function-calling protocol. So this adapter, unlike the generic openai-compat brain:
|
|
8
|
+
* 1. does NOT send `tools`/`tool_choice` (EMBER ignores them); instead it appends a
|
|
9
|
+
* terse output contract + the available tool names to the system prompt, matching
|
|
10
|
+
* what EMBER was trained on (DragonSpark configs/fixtures/system_prompt.txt);
|
|
11
|
+
* 2. parses EMBER's JSON-array output back into the loop's normalized ToolCall shape.
|
|
12
|
+
*
|
|
13
|
+
* This format bridge is doubly important: it makes EMBER's calls EXECUTE in the loop
|
|
14
|
+
* AND makes the emitted ~/.dragon/traces training-grade (the flywheel that improves the
|
|
15
|
+
* next EMBER). Wyrm-RAG comes for free — dragon-cli already folds recalled project
|
|
16
|
+
* context into the system prompt (loop.ts buildSystemPrompt `primed`), so EMBER decides
|
|
17
|
+
* WITH memory, which is the whole "Wyrm is the brain" thesis.
|
|
18
|
+
*
|
|
19
|
+
* Copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
|
|
20
|
+
*/
|
|
21
|
+
import type { Brain, ToolCall } from './types.js';
|
|
22
|
+
/** Pull EMBER's JSON tool array out of the response text (tolerant of stray prose). */
|
|
23
|
+
export declare function parseEmberToolCalls(text: string): ToolCall[];
|
|
24
|
+
export declare function makeGhostEmberBrain(opts: {
|
|
25
|
+
baseURL: string;
|
|
26
|
+
model: string;
|
|
27
|
+
}): Brain;
|
|
28
|
+
//# sourceMappingURL=ghost-ember.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ghost-ember.d.ts","sourceRoot":"","sources":["../../src/brain/ghost-ember.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,KAAK,EAA2B,QAAQ,EAAY,MAAM,YAAY,CAAA;AAuBpF,uFAAuF;AACvF,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,EAAE,CAc5D;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,KAAK,CAgCnF"}
|