@flame0510/project-aether 1.1.9

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 (290) hide show
  1. package/README.md +150 -0
  2. package/agent-templates/README.md +37 -0
  3. package/agent-templates/argus/AGENTS.md +52 -0
  4. package/agent-templates/argus/IDENTITY.md +7 -0
  5. package/agent-templates/argus/MEMORY.md +9 -0
  6. package/agent-templates/argus/SOUL.md +24 -0
  7. package/agent-templates/argus/TOOLS.md +8 -0
  8. package/agent-templates/atlas/AGENTS.md +47 -0
  9. package/agent-templates/atlas/HEARTBEAT.md +10 -0
  10. package/agent-templates/atlas/IDENTITY.md +6 -0
  11. package/agent-templates/atlas/MEMORY.md +9 -0
  12. package/agent-templates/atlas/SOUL.md +21 -0
  13. package/agent-templates/atlas/TOOLS.md +4 -0
  14. package/agent-templates/base-image/.trigger +0 -0
  15. package/agent-templates/base-image/Dockerfile +90 -0
  16. package/agent-templates/base-image/entrypoint.sh +37 -0
  17. package/agent-templates/custom/AGENTS.md +29 -0
  18. package/agent-templates/custom/BOOTSTRAP.md +56 -0
  19. package/agent-templates/custom/MEMORY.md +8 -0
  20. package/agent-templates/prometheus/AGENTS.md +37 -0
  21. package/agent-templates/prometheus/IDENTITY.md +6 -0
  22. package/agent-templates/prometheus/MEMORY.md +9 -0
  23. package/agent-templates/prometheus/SOUL.md +21 -0
  24. package/agent-templates/prometheus/TOOLS.md +4 -0
  25. package/app/agents/ChannelManager.tsx +465 -0
  26. package/app/agents/ImageDownloadBanner.tsx +159 -0
  27. package/app/agents/PageClient.tsx +1285 -0
  28. package/app/agents/create/PageClient.tsx +437 -0
  29. package/app/agents/create/page.tsx +126 -0
  30. package/app/agents/loading.tsx +14 -0
  31. package/app/agents/page.tsx +5 -0
  32. package/app/api/agents/[id]/backup/route.ts +169 -0
  33. package/app/api/agents/[id]/channels/pairing/route.ts +69 -0
  34. package/app/api/agents/[id]/channels/route.ts +14 -0
  35. package/app/api/agents/[id]/channels/telegram/route.ts +50 -0
  36. package/app/api/agents/[id]/lifecycle/route.ts +94 -0
  37. package/app/api/agents/[id]/recreate/route.ts +199 -0
  38. package/app/api/agents/[id]/restart/route.ts +33 -0
  39. package/app/api/agents/[id]/restore/route.ts +81 -0
  40. package/app/api/agents/[id]/route.ts +280 -0
  41. package/app/api/agents/channels-summary/route.ts +39 -0
  42. package/app/api/agents/config-json/route.ts +58 -0
  43. package/app/api/agents/create/route.ts +488 -0
  44. package/app/api/agents/download-image/route.ts +32 -0
  45. package/app/api/agents/image-status/route.ts +126 -0
  46. package/app/api/agents/route.ts +213 -0
  47. package/app/api/agents/token/route.ts +94 -0
  48. package/app/api/agents-active/route.ts +190 -0
  49. package/app/api/agents-config/route.ts +629 -0
  50. package/app/api/alerts/smoke/route.ts +26 -0
  51. package/app/api/aliases/add/route.ts +28 -0
  52. package/app/api/aliases/remove/route.ts +27 -0
  53. package/app/api/assistant/route.ts +204 -0
  54. package/app/api/auth/check/route.ts +38 -0
  55. package/app/api/auth/login/route.ts +85 -0
  56. package/app/api/auth/logout/route.ts +7 -0
  57. package/app/api/auth/status/route.ts +33 -0
  58. package/app/api/config/env/route.ts +132 -0
  59. package/app/api/config/restart/route.ts +26 -0
  60. package/app/api/containers/route.ts +75 -0
  61. package/app/api/cost-override/route.ts +46 -0
  62. package/app/api/costs/route.ts +84 -0
  63. package/app/api/credentials/[id]/reveal/route.ts +21 -0
  64. package/app/api/credentials/[id]/route.ts +52 -0
  65. package/app/api/credentials/[id]/sync/route.ts +58 -0
  66. package/app/api/credentials/detect/route.ts +246 -0
  67. package/app/api/credentials/route.ts +33 -0
  68. package/app/api/crons/[id]/route.ts +69 -0
  69. package/app/api/crons/route.ts +12 -0
  70. package/app/api/crons/runs/route.ts +31 -0
  71. package/app/api/debug/route.ts +11 -0
  72. package/app/api/envcheck/route.ts +18 -0
  73. package/app/api/events/route.ts +24 -0
  74. package/app/api/gateway/agent/route.ts +154 -0
  75. package/app/api/gateway/provider/balance/route.ts +38 -0
  76. package/app/api/gateway/provider/keys.ts +38 -0
  77. package/app/api/gateway/provider/oauth/route.ts +304 -0
  78. package/app/api/gateway/provider/route.ts +209 -0
  79. package/app/api/gateway/route.ts +223 -0
  80. package/app/api/gateway/sync.ts +189 -0
  81. package/app/api/lineage/route.ts +54 -0
  82. package/app/api/memory-context/route.ts +15 -0
  83. package/app/api/metrics/route.ts +35 -0
  84. package/app/api/models/route.ts +77 -0
  85. package/app/api/plugins/route.js +37 -0
  86. package/app/api/provider/upstream.ts +142 -0
  87. package/app/api/provider/v1/chat/completions/route.ts +146 -0
  88. package/app/api/provider/v1/models/route.ts +167 -0
  89. package/app/api/provider-usage/route.ts +629 -0
  90. package/app/api/session/route.ts +39 -0
  91. package/app/api/sessions/[id]/route.ts +38 -0
  92. package/app/api/sessions/route.ts +23 -0
  93. package/app/api/setup/agent-image/route.ts +102 -0
  94. package/app/api/setup/password/route.ts +111 -0
  95. package/app/api/setup/restart/route.ts +76 -0
  96. package/app/api/skills/delete/route.js +105 -0
  97. package/app/api/skills/promote/route.js +129 -0
  98. package/app/api/skills/route.js +203 -0
  99. package/app/api/skills/save/route.js +142 -0
  100. package/app/api/stats/route.ts +34 -0
  101. package/app/api/stats-since/route.ts +20 -0
  102. package/app/api/stream/route.ts +70 -0
  103. package/app/api/system-health/route.ts +233 -0
  104. package/app/api/tool-calls/route.ts +24 -0
  105. package/app/api/tools-config/route.ts +163 -0
  106. package/app/api/update-check/route.ts +127 -0
  107. package/app/api/version/route.ts +25 -0
  108. package/app/api/wizard/complete/route.ts +32 -0
  109. package/app/api/wizard/reset/route.ts +28 -0
  110. package/app/api/wizard/status/route.ts +28 -0
  111. package/app/api/workspace/route.ts +392 -0
  112. package/app/api/workspace/stream/route.ts +136 -0
  113. package/app/components/AppShell.tsx +33 -0
  114. package/app/components/AuthGuard.tsx +91 -0
  115. package/app/components/CostBreakdown.tsx +37 -0
  116. package/app/components/DashboardHeader.tsx +73 -0
  117. package/app/components/DashboardLayout.tsx +76 -0
  118. package/app/components/DashboardToolbar.tsx +61 -0
  119. package/app/components/FloatingActions.tsx +33 -0
  120. package/app/components/Icons.tsx +78 -0
  121. package/app/components/LineageGraphPage.tsx +142 -0
  122. package/app/components/LiveFeed.tsx +48 -0
  123. package/app/components/MobileBottomNav.tsx +84 -0
  124. package/app/components/ModelPickerModal.tsx +94 -0
  125. package/app/components/PasswordInput.tsx +86 -0
  126. package/app/components/PulseChat.tsx +362 -0
  127. package/app/components/Rev4aLoader.tsx +48 -0
  128. package/app/components/SessionDrawer.tsx +263 -0
  129. package/app/components/SessionTopology.tsx +289 -0
  130. package/app/components/Sidebar.tsx +294 -0
  131. package/app/components/Skeleton.tsx +133 -0
  132. package/app/components/SystemCockpit.tsx +169 -0
  133. package/app/components/VersionBanner.tsx +105 -0
  134. package/app/components/WizardButton.tsx +30 -0
  135. package/app/components/ui/Badge.tsx +30 -0
  136. package/app/components/ui/Button.tsx +59 -0
  137. package/app/components/ui/ConfirmModal.tsx +46 -0
  138. package/app/components/ui/FilterBar.tsx +33 -0
  139. package/app/components/ui/Input.tsx +25 -0
  140. package/app/components/ui/ItemList.tsx +83 -0
  141. package/app/components/ui/LoadingSpinner.tsx +30 -0
  142. package/app/components/ui/Metric.tsx +22 -0
  143. package/app/components/ui/Modal.tsx +42 -0
  144. package/app/components/ui/Page.tsx +18 -0
  145. package/app/components/ui/Pill.tsx +6 -0
  146. package/app/components/ui/PropertyList.tsx +24 -0
  147. package/app/components/ui/Select.tsx +26 -0
  148. package/app/components/ui/StatusCard.tsx +32 -0
  149. package/app/components/ui/Surface.tsx +14 -0
  150. package/app/components/ui/Tabs.tsx +55 -0
  151. package/app/components/ui/TemplateOption.tsx +59 -0
  152. package/app/components/ui/Toast.tsx +44 -0
  153. package/app/components/ui/index.ts +19 -0
  154. package/app/components/ui/tokens.ts +18 -0
  155. package/app/config/PageClient.tsx +293 -0
  156. package/app/config/loading.tsx +10 -0
  157. package/app/config/page.tsx +7 -0
  158. package/app/containers/ContainersClient.tsx +129 -0
  159. package/app/containers/loading.tsx +21 -0
  160. package/app/containers/page.tsx +5 -0
  161. package/app/containers/terminal/[id]/TerminalClient.tsx +313 -0
  162. package/app/containers/terminal/[id]/page.tsx +6 -0
  163. package/app/credentials/PageClient.tsx +598 -0
  164. package/app/credentials/loading.tsx +10 -0
  165. package/app/credentials/page.tsx +7 -0
  166. package/app/crons/PageClient.tsx +538 -0
  167. package/app/crons/loading.tsx +10 -0
  168. package/app/crons/page.tsx +5 -0
  169. package/app/design-system.ts +57 -0
  170. package/app/gateway/PageClient.tsx +1048 -0
  171. package/app/gateway/loading.tsx +24 -0
  172. package/app/gateway/page.tsx +5 -0
  173. package/app/globals.css +1474 -0
  174. package/app/layout.tsx +45 -0
  175. package/app/lib/models-context.tsx +41 -0
  176. package/app/lineage/loading.tsx +10 -0
  177. package/app/lineage/page.tsx +13 -0
  178. package/app/loading.tsx +16 -0
  179. package/app/login/page.tsx +160 -0
  180. package/app/memory/MemoryContextPageClient.tsx +196 -0
  181. package/app/memory/loading.tsx +10 -0
  182. package/app/memory/page.tsx +8 -0
  183. package/app/page.tsx +31 -0
  184. package/app/plugins/PageClient.tsx +296 -0
  185. package/app/plugins/loading.tsx +10 -0
  186. package/app/plugins/page.tsx +5 -0
  187. package/app/setup/PageClient.tsx +227 -0
  188. package/app/setup/loading.tsx +21 -0
  189. package/app/setup/page.tsx +7 -0
  190. package/app/skills/PageClient.tsx +499 -0
  191. package/app/skills/loading.tsx +10 -0
  192. package/app/skills/page.tsx +5 -0
  193. package/app/tools/ToolsPageClient.tsx +308 -0
  194. package/app/tools/loading.tsx +10 -0
  195. package/app/tools/page.tsx +62 -0
  196. package/app/tools/tools-catalog.ts +141 -0
  197. package/app/wizard/PageClient.tsx +485 -0
  198. package/app/wizard/icons.tsx +89 -0
  199. package/app/wizard/loading.tsx +24 -0
  200. package/app/wizard/page.tsx +7 -0
  201. package/app/wizard/storage.ts +33 -0
  202. package/app/wizard/useWizard.ts +101 -0
  203. package/app/workspace/WorkspaceClient.tsx +633 -0
  204. package/app/workspace/loading.tsx +36 -0
  205. package/app/workspace/page.tsx +8 -0
  206. package/bin/docker-entrypoint.sh +21 -0
  207. package/bin/postinstall.js +20 -0
  208. package/bin/rev4a.js +696 -0
  209. package/daemon.js +783 -0
  210. package/docs/ARCHITECTURE.md +557 -0
  211. package/docs/CONTAINER-TERMINAL.md +280 -0
  212. package/docs/DESIGN-SYSTEM.md +99 -0
  213. package/docs/FRONTEND-ARCHITECTURE.md +119 -0
  214. package/docs/REV4A.md +504 -0
  215. package/docs/dev/API-REFERENCE.md +1835 -0
  216. package/docs/dev/DATABASE.md +224 -0
  217. package/docs/dev/GATEWAY.md +300 -0
  218. package/docs/dev/PROVIDERS.md +229 -0
  219. package/docs/dev/SESSION-MAINTENANCE-PLAN.md +607 -0
  220. package/docs/dev/WORKSPACE.md +199 -0
  221. package/docs/rag/DATA-FRESHNESS.md +109 -0
  222. package/docs/rag/GLOSSARY.md +125 -0
  223. package/docs/rag/REV4A-OVERVIEW.md +135 -0
  224. package/docs/rag/WHAT-I-CAN-ANSWER.md +124 -0
  225. package/lib/agent-setup.ts +130 -0
  226. package/lib/alerts.ts +115 -0
  227. package/lib/apiFetch.ts +7 -0
  228. package/lib/auth.ts +28 -0
  229. package/lib/billing.ts +100 -0
  230. package/lib/buildAgentImage.ts +346 -0
  231. package/lib/channelManager.ts +386 -0
  232. package/lib/container.ts +60 -0
  233. package/lib/credentials/db.ts +86 -0
  234. package/lib/credentials/delivery.ts +448 -0
  235. package/lib/credentials/detect.ts +149 -0
  236. package/lib/credentials/hash.ts +9 -0
  237. package/lib/credentials/providers.ts +110 -0
  238. package/lib/credentials/vault.ts +195 -0
  239. package/lib/db-bootstrap.d.ts +3 -0
  240. package/lib/db-bootstrap.mjs +144 -0
  241. package/lib/db.ts +25 -0
  242. package/lib/docker-utils.ts +60 -0
  243. package/lib/hooks/useDashboard.ts +264 -0
  244. package/lib/hooks/useRev4aTimezone.ts +28 -0
  245. package/lib/hooks/useScreenshot.ts +30 -0
  246. package/lib/memory-context.ts +385 -0
  247. package/lib/model-catalogue.ts +131 -0
  248. package/lib/model-pricing.ts +63 -0
  249. package/lib/openclaw-cron.ts +350 -0
  250. package/lib/patterns/ApiAdapter.ts +73 -0
  251. package/lib/patterns/ApiClient.ts +65 -0
  252. package/lib/patterns/EventBus.ts +113 -0
  253. package/lib/patterns/FilterStrategy.ts +79 -0
  254. package/lib/patterns/SessionFactory.ts +117 -0
  255. package/lib/patterns/sessionPresentation.ts +117 -0
  256. package/lib/provider-balance.ts +163 -0
  257. package/lib/requireAuth.tsx +38 -0
  258. package/lib/rev4a-auth.d.ts +3 -0
  259. package/lib/rev4a-auth.js +36 -0
  260. package/lib/rev4a-paths.ts +145 -0
  261. package/lib/timezone.ts +46 -0
  262. package/lib/types/index.ts +96 -0
  263. package/lib/utils/format.ts +82 -0
  264. package/lineage.js +56 -0
  265. package/model-pricing.json +482 -0
  266. package/models.config.json +844 -0
  267. package/next-env.d.ts +6 -0
  268. package/next.config.mjs +7 -0
  269. package/package.json +82 -0
  270. package/public/apple-touch-icon.png +0 -0
  271. package/public/favicon-64.png +0 -0
  272. package/public/favicon.svg +58 -0
  273. package/public/icon-192-maskable.png +0 -0
  274. package/public/icon-192.png +0 -0
  275. package/public/icon-512-maskable.png +0 -0
  276. package/public/icon-512.png +0 -0
  277. package/public/manifest.json +38 -0
  278. package/public/rev4a-logo.png +0 -0
  279. package/public/rev4a-logo.svg +58 -0
  280. package/rev4a-rules/AGENTS.md +37 -0
  281. package/scripts/add-auth.cjs +185 -0
  282. package/scripts/backup.sh +66 -0
  283. package/scripts/check-providers.mjs +143 -0
  284. package/scripts/docker/traefik/docker-compose.yml +32 -0
  285. package/scripts/docker/traefik/dynamic/.gitkeep +18 -0
  286. package/scripts/docker/traefik/traefik.yml +37 -0
  287. package/scripts/remove-auth-profile.js +56 -0
  288. package/scripts/restore.sh +82 -0
  289. package/terminal-ws-server.js +139 -0
  290. package/tsconfig.json +42 -0
@@ -0,0 +1,18 @@
1
+ import { type NextRequest } from "next/server";
2
+ import { NextResponse } from 'next/server';
3
+ import { requireAuthJWT } from '@/lib/rev4a-auth';
4
+
5
+ const SENSITIVE = ['REV4A_PASSWORD', 'REV4A_TOKEN', 'REV4A_JWT_SECRET'];
6
+
7
+ export async function GET(request: NextRequest) {
8
+ const safe: Record<string, string> = {};
9
+ for (const key of ['REV4A_PASSWORD', 'REV4A_TOKEN', 'REV4A_JWT_SECRET', 'REV4A_ALERTS_ENABLED', 'REV4A_DB', 'REV4A_WORKSPACE_ROOT', 'PROVIDER_DEEPSEEK_API_KEY', 'NODE_ENV']) {
10
+ const val = process.env[key];
11
+ if (SENSITIVE.includes(key)) {
12
+ safe[key] = val ? '✅ set (' + val.substring(0, 4) + '...)' : '❌ not set';
13
+ } else {
14
+ safe[key] = val || '(not set)';
15
+ }
16
+ }
17
+ return NextResponse.json(safe);
18
+ }
@@ -0,0 +1,24 @@
1
+ import { NextResponse, type NextRequest } from 'next/server';
2
+ import { openDb } from '@/lib/db';
3
+ import { requireAuthJWT } from '@/lib/rev4a-auth';
4
+
5
+ export async function GET(request: NextRequest): Promise<NextResponse> {
6
+ const denied = await requireAuthJWT(request); if (denied) return denied as any;
7
+ const { searchParams } = new URL(request.url);
8
+ const parsed = Number.parseInt(searchParams.get('limit') ?? '50', 10);
9
+ const limit = Number.isFinite(parsed) ? Math.min(Math.max(parsed, 1), 500) : 50;
10
+ try {
11
+ const db = openDb();
12
+ const events = db
13
+ .prepare(
14
+ `SELECT e.*, s.label AS session_label
15
+ FROM events e LEFT JOIN sessions s ON s.session_id = e.session_id
16
+ ORDER BY e.ts DESC LIMIT ?`,
17
+ )
18
+ .all(limit);
19
+ db.close();
20
+ return NextResponse.json(events);
21
+ } catch (e: unknown) {
22
+ return NextResponse.json({ error: (e as Error).message }, { status: 500 });
23
+ }
24
+ }
@@ -0,0 +1,154 @@
1
+ /**
2
+ * PUT /api/gateway/agent
3
+ *
4
+ * Update the model configuration for an agent container.
5
+ * Uses base64-safe write — no heredoc shell escaping issues.
6
+ *
7
+ * Body:
8
+ * { "containerName": "openclaw-atlas", "model": "rev4a/deepseek-v4-flash", "fallbacks": ["rev4a/deepseek-v4-pro"] }
9
+ */
10
+ import { NextRequest, NextResponse } from 'next/server';
11
+ import { execSync } from 'child_process';
12
+ import { requireAuthJWT } from '@/lib/rev4a-auth';
13
+
14
+ export const dynamic = 'force-dynamic';
15
+ export const runtime = 'nodejs';
16
+
17
+ function execInContainer(container: string, cmd: string, timeout = 15000): string {
18
+ try {
19
+ return execSync(
20
+ `docker exec ${container} sh -c ${JSON.stringify(cmd)}`,
21
+ { timeout, maxBuffer: 1024 * 1024, encoding: 'utf-8' },
22
+ ).trim();
23
+ } catch (e: unknown) {
24
+ const err = e as { stderr?: string; message?: string };
25
+ throw new Error(err.stderr || err.message || `Failed to exec in ${container}`);
26
+ }
27
+ }
28
+
29
+ function ensureRev4aPrefix(id: string): string {
30
+ return id.startsWith('rev4a/') ? id : `rev4a/${id}`;
31
+ }
32
+
33
+ function ensureRev4aPrefixes(ids: string[]): string[] {
34
+ return ids.map(ensureRev4aPrefix);
35
+ }
36
+
37
+ function dedupe<T>(arr: T[]): T[] {
38
+ return [...new Set(arr)];
39
+ }
40
+
41
+ /** Write content to a file inside a container. */
42
+ function writeFileToContainer(container: string, remotePath: string, content: string): void {
43
+ const b64 = Buffer.from(content, 'utf-8').toString('base64');
44
+ execSync(
45
+ `docker exec ${container} sh -c "echo '${b64}' | base64 -d > ${remotePath}"`,
46
+ { timeout: 15000, maxBuffer: 128 * 1024 },
47
+ );
48
+ }
49
+
50
+ /** Read file content from container (stderr goes to /dev/null). */
51
+ function readFileFromContainer(container: string, remotePath: string): string {
52
+ try {
53
+ return execInContainer(container, `cat ${remotePath} 2>/dev/null`);
54
+ } catch {
55
+ return '';
56
+ }
57
+ }
58
+
59
+ export async function PUT(request: NextRequest) {
60
+ const denied = await requireAuthJWT(request); if (denied) return denied as any;
61
+ try {
62
+ const body: { containerName?: string; model?: string; fallbacks?: string[] } = await request.json();
63
+ const { containerName } = body;
64
+
65
+ if (!containerName) {
66
+ return NextResponse.json({ status: 'error', error: 'containerName is required' }, { status: 400 });
67
+ }
68
+
69
+ const safeContainer = containerName.replace(/[^a-zA-Z0-9_.-]/g, '');
70
+ if (safeContainer !== containerName) {
71
+ return NextResponse.json({ status: 'error', error: 'Invalid container name' }, { status: 400 });
72
+ }
73
+
74
+ if (!body.model) {
75
+ return NextResponse.json({ status: 'error', error: 'model is required' }, { status: 400 });
76
+ }
77
+
78
+ // Read current config
79
+ const raw = readFileFromContainer(safeContainer, '/root/.openclaw/openclaw.json');
80
+ if (!raw) {
81
+ return NextResponse.json({ status: 'error', error: 'Failed to read config from container (empty)' }, { status: 500 });
82
+ }
83
+
84
+ let config: Record<string, unknown>;
85
+ try {
86
+ config = JSON.parse(raw);
87
+ } catch (e) {
88
+ return NextResponse.json({
89
+ status: 'error',
90
+ error: 'Failed to parse config from container',
91
+ detail: raw.slice(0, 200),
92
+ }, { status: 500 });
93
+ }
94
+
95
+ // Update model config
96
+ const agents = (config.agents || {}) as Record<string, unknown>;
97
+ const list = (agents.list || []) as Record<string, unknown>[];
98
+
99
+ const mainAgent = list.find((a) => a.id === 'main');
100
+ if (mainAgent) {
101
+ const primary = ensureRev4aPrefix(body.model);
102
+ let fallbacks = dedupe(ensureRev4aPrefixes(body.fallbacks || []));
103
+ // Remove the primary model dai fallback
104
+ fallbacks = fallbacks.filter((fb) => fb !== primary);
105
+ mainAgent.model = {
106
+ primary,
107
+ fallbacks,
108
+ };
109
+ }
110
+
111
+ const defaults = (agents.defaults || {}) as Record<string, unknown>;
112
+ {
113
+ const primary = ensureRev4aPrefix(body.model);
114
+ let fallbacks = dedupe(ensureRev4aPrefixes(body.fallbacks || []));
115
+ fallbacks = fallbacks.filter((fb) => fb !== primary);
116
+ defaults.model = {
117
+ primary,
118
+ fallbacks,
119
+ };
120
+ }
121
+
122
+ // Write back
123
+ const newConfig = JSON.stringify(config, null, 2);
124
+ execSync(
125
+ `docker exec -i ${safeContainer} sh -c 'cat > /root/.openclaw/openclaw.json'`,
126
+ { timeout: 15000, maxBuffer: 1024 * 1024, input: newConfig },
127
+ );
128
+
129
+ // Restart gateway to pick up the changes
130
+ try {
131
+ execSync(`docker exec ${safeContainer} openclaw gateway restart`, { timeout: 15000, maxBuffer: 64 * 1024 });
132
+ } catch {
133
+ // restart failure is non-fatal, config was written
134
+ }
135
+
136
+ // Verify
137
+ const verifyRaw = readFileFromContainer(safeContainer, '/root/.openclaw/openclaw.json');
138
+ const verifyOk = verifyRaw.includes('"primary"');
139
+ const verifyBytes = verifyRaw.length;
140
+
141
+ return NextResponse.json({
142
+ status: 'ok',
143
+ agent: safeContainer,
144
+ model: {
145
+ primary: body.model,
146
+ fallbacks: body.fallbacks || [],
147
+ },
148
+ verify: { ok: verifyOk, bytes: verifyBytes },
149
+ });
150
+ } catch (e: unknown) {
151
+ const message = e instanceof Error ? e.message : String(e);
152
+ return NextResponse.json({ status: 'error', error: message }, { status: 500 });
153
+ }
154
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * GET /api/gateway/provider/balance?provider=deepseek
3
+ *
4
+ * Fetches the current balance/credits from an upstream AI provider.
5
+ * Requires the provider's API key to be configured in provider-keys.json.
6
+ *
7
+ * Supported providers: deepseek, openrouter, glm
8
+ */
9
+ import { NextRequest, NextResponse } from 'next/server';
10
+ import { fetchProviderBalance, type BalanceProvider } from '@/lib/provider-balance';
11
+ import { requireAuthJWT } from '@/lib/rev4a-auth';
12
+
13
+ export const dynamic = 'force-dynamic';
14
+ export const runtime = 'nodejs';
15
+
16
+ const SUPPORTED: BalanceProvider[] = ['deepseek', 'openrouter', 'glm'];
17
+
18
+ export async function GET(request: NextRequest): Promise<NextResponse> {
19
+ const denied = await requireAuthJWT(request); if (denied) return denied as any;
20
+ const provider = request.nextUrl.searchParams.get('provider') as BalanceProvider | null;
21
+
22
+ if (!provider || !SUPPORTED.includes(provider)) {
23
+ return NextResponse.json(
24
+ {
25
+ error: `Unsupported or missing provider. Supported: ${SUPPORTED.join(', ')}`,
26
+ },
27
+ { status: 400 },
28
+ );
29
+ }
30
+
31
+ try {
32
+ const result = await fetchProviderBalance(provider);
33
+ return NextResponse.json({ provider, ...result });
34
+ } catch (e: unknown) {
35
+ const message = e instanceof Error ? e.message : String(e);
36
+ return NextResponse.json({ provider, error: message }, { status: 502 });
37
+ }
38
+ }
@@ -0,0 +1,38 @@
1
+ import * as fs from 'fs';
2
+ import { PROVIDER_KEYS_FILE } from '@/lib/rev4a-paths';
3
+ import * as path from 'path';
4
+
5
+ const KEYS_PATH = PROVIDER_KEYS_FILE;
6
+
7
+ interface ProviderKeys {
8
+ [provider: string]: string;
9
+ }
10
+
11
+ function ensureDir(): void {
12
+ const dir = path.dirname(KEYS_PATH);
13
+ if (!fs.existsSync(dir)) {
14
+ fs.mkdirSync(dir, { recursive: true });
15
+ }
16
+ }
17
+
18
+ export function readProviderKeys(): ProviderKeys {
19
+ ensureDir();
20
+ try {
21
+ const raw = fs.readFileSync(KEYS_PATH, 'utf-8');
22
+ return JSON.parse(raw);
23
+ } catch {
24
+ // File doesn't exist — try to migrate from env
25
+ const keys: ProviderKeys = {};
26
+ if (process.env.PROVIDER_DEEPSEEK_API_KEY) keys['deepseek'] = process.env.PROVIDER_DEEPSEEK_API_KEY;
27
+ if (process.env.PROVIDER_OPENROUTER_API_KEY) keys['openrouter'] = process.env.PROVIDER_OPENROUTER_API_KEY;
28
+ if (Object.keys(keys).length > 0) {
29
+ writeProviderKeys(keys);
30
+ }
31
+ return keys;
32
+ }
33
+ }
34
+
35
+ export function writeProviderKeys(keys: ProviderKeys): void {
36
+ ensureDir();
37
+ fs.writeFileSync(KEYS_PATH, JSON.stringify(keys, null, 2), 'utf-8');
38
+ }
@@ -0,0 +1,304 @@
1
+ /**
2
+ * Gateway OAuth — Device Code Flow
3
+ *
4
+ * POST /api/gateway/provider/oauth — start device code flow
5
+ * GET /api/gateway/provider/oauth — poll authorization status
6
+ * POST /api/gateway/provider/oauth/save — save completed token
7
+ *
8
+ * Designed for multiple providers (openai-codex, github-copilot, claude-cli, etc.)
9
+ * Each provider defines its own device code config + token exchange logic.
10
+ *
11
+ * After successful auth, the API key is saved to provider-keys.json
12
+ * and the provider appears automatically in /api/models and Pulse.
13
+ */
14
+ import { NextRequest, NextResponse } from 'next/server';
15
+ import { readProviderKeys, writeProviderKeys } from '@/app/api/gateway/provider/keys';
16
+ import { requireAuthJWT } from '@/lib/rev4a-auth';
17
+
18
+ export const dynamic = 'force-dynamic';
19
+
20
+ // ── Provider OAuth configs ─────────────────────────────────────────────
21
+
22
+ interface DeviceCodeConfig {
23
+ clientId: string;
24
+ clientSecret?: string;
25
+ deviceCodeUrl: string;
26
+ pollUrl: string;
27
+ tokenUrl: string;
28
+ verificationUrl: string;
29
+ verificationTitle?: string;
30
+ defaultIntervalMs: number;
31
+ /** How to authenticate in the token exchange */
32
+ authStyle: 'basic' | 'json' | 'form';
33
+ /** Extra headers to send */
34
+ extraHeaders?: Record<string, string>;
35
+ /** Custom token exchange logic — returns { accessToken, refreshToken?, expiresMs? } */
36
+ exchangeTokens?: (data: Record<string, unknown>, config: DeviceCodeConfig) => Promise<OAuthTokenResult>;
37
+ /** Display-friendly name */
38
+ label: string;
39
+ }
40
+
41
+ interface OAuthTokenResult {
42
+ accessToken: string;
43
+ refreshToken?: string;
44
+ expiresMs?: number;
45
+ }
46
+
47
+ const OAUTH_CONFIGS: Record<string, DeviceCodeConfig> = {
48
+ 'openai-codex': {
49
+ label: 'OpenAI Codex',
50
+ clientId: 'app_EMoamEEZ73f0CkXaXp7hrann',
51
+ deviceCodeUrl: 'https://auth.openai.com/api/accounts/deviceauth/usercode',
52
+ pollUrl: 'https://auth.openai.com/api/accounts/deviceauth/token',
53
+ tokenUrl: 'https://auth.openai.com/oauth/token',
54
+ verificationUrl: 'https://auth.openai.com/codex/device',
55
+ verificationTitle: 'auth.openai.com',
56
+ defaultIntervalMs: 5000,
57
+ authStyle: 'json',
58
+ extraHeaders: {
59
+ originator: 'openclaw',
60
+ 'User-Agent': 'openclaw/rev4a',
61
+ },
62
+ },
63
+
64
+ 'github-copilot': {
65
+ label: 'GitHub Copilot',
66
+ clientId: 'Iv23lirMpKoFUugB9w4p',
67
+ deviceCodeUrl: 'https://github.com/login/device/code',
68
+ pollUrl: 'https://github.com/login/oauth/access_token',
69
+ tokenUrl: 'https://github.com/login/oauth/access_token',
70
+ verificationUrl: 'https://github.com/login/device',
71
+ verificationTitle: 'github.com',
72
+ defaultIntervalMs: 5000,
73
+ authStyle: 'json',
74
+ },
75
+
76
+ // Placeholder for future providers:
77
+ // 'claude-cli': {
78
+ // label: 'Claude CLI',
79
+ // clientId: '...',
80
+ // ...
81
+ // },
82
+ };
83
+
84
+ // ── START device code flow ─────────────────────────────────────────────
85
+
86
+ export async function POST(request: NextRequest): Promise<NextResponse> {
87
+ const denied = await requireAuthJWT(request); if (denied) return denied as any;
88
+ try {
89
+ const body = await request.json().catch(() => ({}));
90
+ const provider = body.provider as string | undefined;
91
+
92
+ if (!provider || !OAUTH_CONFIGS[provider]) {
93
+ const available = Object.keys(OAUTH_CONFIGS);
94
+ return NextResponse.json({
95
+ error: `Unknown or unsupported provider: '${provider}'`,
96
+ available,
97
+ }, { status: 400 });
98
+ }
99
+
100
+ const config = OAUTH_CONFIGS[provider];
101
+
102
+ // Step 1: request device code
103
+ const deviceRes = await fetch(config.deviceCodeUrl, {
104
+ method: 'POST',
105
+ headers: {
106
+ 'Content-Type': 'application/json',
107
+ ...config.extraHeaders,
108
+ },
109
+ body: JSON.stringify({ client_id: config.clientId }),
110
+ });
111
+
112
+ if (!deviceRes.ok) {
113
+ const errText = await deviceRes.text();
114
+ return NextResponse.json({
115
+ status: 'failed',
116
+ error: `Device code request failed: HTTP ${deviceRes.status} — ${errText.slice(0, 300)}`,
117
+ }, { status: 502 });
118
+ }
119
+
120
+ const deviceData = await deviceRes.json() as Record<string, unknown>;
121
+ const deviceAuthId = (deviceData.device_auth_id || deviceData.device_code) as string | undefined;
122
+ const userCode = (deviceData.user_code || deviceData.usercode) as string | undefined;
123
+
124
+ if (!deviceAuthId || !userCode) {
125
+ return NextResponse.json({
126
+ status: 'failed',
127
+ error: 'Provider response missing device_auth_id or user_code',
128
+ raw: deviceData,
129
+ }, { status: 502 });
130
+ }
131
+
132
+ const intervalMs = typeof deviceData.interval === 'number'
133
+ ? deviceData.interval * 1000
134
+ : config.defaultIntervalMs;
135
+
136
+ return NextResponse.json({
137
+ status: 'pending',
138
+ provider,
139
+ label: config.label,
140
+ deviceAuthId,
141
+ userCode,
142
+ verificationUri: config.verificationUrl,
143
+ verificationTitle: config.verificationTitle || new URL(config.verificationUrl).hostname,
144
+ intervalMs,
145
+ expiresMs: Date.now() + 15 * 60 * 1000, // 15 min timeout
146
+ });
147
+ } catch (e: unknown) {
148
+ const message = e instanceof Error ? e.message : String(e);
149
+ return NextResponse.json({ status: 'failed', error: message }, { status: 500 });
150
+ }
151
+ }
152
+
153
+ // ── POLL authorization status ──────────────────────────────────────────
154
+
155
+ export async function GET(request: NextRequest): Promise<NextResponse> {
156
+ const denied = await requireAuthJWT(request); if (denied) return denied as any;
157
+ try {
158
+ const provider = request.nextUrl.searchParams.get('provider');
159
+ const deviceAuthId = request.nextUrl.searchParams.get('deviceAuthId');
160
+ const userCode = request.nextUrl.searchParams.get('userCode') || '';
161
+ const expiresMs = parseInt(request.nextUrl.searchParams.get('expiresMs') || '0');
162
+
163
+ if (!provider || !deviceAuthId) {
164
+ return NextResponse.json({ status: 'missing_params', error: 'provider and deviceAuthId required' }, { status: 400 });
165
+ }
166
+
167
+ // Check timeout
168
+ if (expiresMs && Date.now() > expiresMs) {
169
+ return NextResponse.json({ status: 'timeout', message: 'Authorization timed out. Please start again.' });
170
+ }
171
+
172
+ const config = OAUTH_CONFIGS[provider];
173
+ if (!config) {
174
+ return NextResponse.json({ status: 'unsupported', error: `Unknown provider: ${provider}` }, { status: 400 });
175
+ }
176
+
177
+ // Poll
178
+ const pollHeaders: Record<string, string> = {
179
+ 'Content-Type': 'application/json',
180
+ ...config.extraHeaders,
181
+ };
182
+
183
+ const pollBody: Record<string, string> = {
184
+ device_auth_id: deviceAuthId,
185
+ user_code: userCode,
186
+ };
187
+
188
+ if (config.authStyle === 'json') {
189
+ pollBody.client_id = config.clientId;
190
+ }
191
+
192
+ const pollRes = await fetch(config.pollUrl, {
193
+ method: 'POST',
194
+ headers: pollHeaders,
195
+ body: JSON.stringify(pollBody),
196
+ });
197
+
198
+ // Authorization still pending — provider returns 403/40x
199
+ if (!pollRes.ok) {
200
+ if (pollRes.status === 403 || pollRes.status === 404) {
201
+ return NextResponse.json({ status: 'pending' });
202
+ }
203
+
204
+ const errText = await pollRes.text();
205
+ try {
206
+ const err = JSON.parse(errText);
207
+ if (err.error === 'authorization_pending') {
208
+ return NextResponse.json({ status: 'pending' });
209
+ }
210
+ if (err.error === 'slow_down') {
211
+ return NextResponse.json({ status: 'pending', delayMs: 10000 });
212
+ }
213
+ } catch {
214
+ // not JSON, fall through
215
+ }
216
+
217
+ return NextResponse.json({
218
+ status: 'failed',
219
+ error: `Poll failed: HTTP ${pollRes.status} — ${errText.slice(0, 300)}`,
220
+ }, { status: 502 });
221
+ }
222
+
223
+ // Success! Parse tokens
224
+ const data = await pollRes.json() as Record<string, unknown>;
225
+
226
+ // OpenAI Codex: returns authorization_code + code_verifier, needs exchange
227
+ if (provider === 'openai-codex' && data.authorization_code && data.code_verifier) {
228
+ const exchangeBody = new URLSearchParams();
229
+ exchangeBody.append('grant_type', 'authorization_code');
230
+ exchangeBody.append('code', data.authorization_code as string);
231
+ exchangeBody.append('redirect_uri', 'https://auth.openai.com/deviceauth/callback');
232
+ exchangeBody.append('client_id', config.clientId);
233
+ exchangeBody.append('code_verifier', data.code_verifier as string);
234
+
235
+ const exchangeHeaders: Record<string, string> = {
236
+ 'Content-Type': 'application/x-www-form-urlencoded',
237
+ ...config.extraHeaders,
238
+ };
239
+
240
+ const exchangeRes = await fetch(config.tokenUrl, {
241
+ method: 'POST',
242
+ headers: exchangeHeaders,
243
+ body: exchangeBody,
244
+ });
245
+
246
+ if (!exchangeRes.ok) {
247
+ const errText = await exchangeRes.text();
248
+ return NextResponse.json({
249
+ status: 'failed',
250
+ error: `Token exchange failed: HTTP ${exchangeRes.status} — ${errText.slice(0, 300)}`,
251
+ }, { status: 502 });
252
+ }
253
+
254
+ const tokenData = await exchangeRes.json() as Record<string, unknown>;
255
+ const accessToken = tokenData.access_token as string;
256
+ const refreshToken = tokenData.refresh_token as string;
257
+
258
+ if (!accessToken) {
259
+ return NextResponse.json({ status: 'failed', error: 'No access_token in exchange response' }, { status: 502 });
260
+ }
261
+
262
+ // Save to provider-keys.json
263
+ const keys = readProviderKeys();
264
+ keys[provider] = accessToken;
265
+ writeProviderKeys(keys);
266
+
267
+ const expiresIn = tokenData.expires_in as number | undefined;
268
+
269
+ return NextResponse.json({
270
+ status: 'completed',
271
+ provider,
272
+ label: config.label,
273
+ refreshToken: refreshToken || null,
274
+ expiresMs: expiresIn ? Date.now() + expiresIn * 1000 : null,
275
+ });
276
+ }
277
+
278
+ // Direct access_token providers (github-copilot, etc.)
279
+ if (data.access_token) {
280
+ const accessToken = data.access_token as string;
281
+
282
+ // Save to provider-keys.json
283
+ const keys = readProviderKeys();
284
+ keys[provider] = accessToken;
285
+ writeProviderKeys(keys);
286
+
287
+ const expiresIn = data.expires_in as number | undefined;
288
+
289
+ return NextResponse.json({
290
+ status: 'completed',
291
+ provider,
292
+ label: config.label,
293
+ refreshToken: (data.refresh_token as string) || null,
294
+ expiresMs: expiresIn ? Date.now() + expiresIn * 1000 : null,
295
+ });
296
+ }
297
+
298
+ // Still waiting
299
+ return NextResponse.json({ status: 'pending' });
300
+ } catch (e: unknown) {
301
+ const message = e instanceof Error ? e.message : String(e);
302
+ return NextResponse.json({ status: 'failed', error: message }, { status: 500 });
303
+ }
304
+ }