@hybridaione/hybridclaw 0.1.5

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 (196) hide show
  1. package/.env.example +14 -0
  2. package/.github/workflows/pages.yml +41 -0
  3. package/AGENTS.md +24 -0
  4. package/CHANGELOG.md +113 -0
  5. package/LICENSE +21 -0
  6. package/README.md +226 -0
  7. package/SECURITY.md +67 -0
  8. package/config.example.json +62 -0
  9. package/container/Dockerfile +23 -0
  10. package/container/package-lock.json +246 -0
  11. package/container/package.json +17 -0
  12. package/container/src/hybridai-client.ts +38 -0
  13. package/container/src/index.ts +198 -0
  14. package/container/src/ipc.ts +37 -0
  15. package/container/src/tools.ts +1008 -0
  16. package/container/src/types.ts +74 -0
  17. package/container/src/web-fetch.ts +389 -0
  18. package/container/tsconfig.json +16 -0
  19. package/dist/agent.d.ts +3 -0
  20. package/dist/agent.d.ts.map +1 -0
  21. package/dist/agent.js +21 -0
  22. package/dist/agent.js.map +1 -0
  23. package/dist/cli.d.ts +3 -0
  24. package/dist/cli.d.ts.map +1 -0
  25. package/dist/cli.js +32 -0
  26. package/dist/cli.js.map +1 -0
  27. package/dist/config.d.ts +38 -0
  28. package/dist/config.d.ts.map +1 -0
  29. package/dist/config.js +107 -0
  30. package/dist/config.js.map +1 -0
  31. package/dist/container-runner.d.ts +11 -0
  32. package/dist/container-runner.d.ts.map +1 -0
  33. package/dist/container-runner.js +246 -0
  34. package/dist/container-runner.js.map +1 -0
  35. package/dist/conversation.d.ts +18 -0
  36. package/dist/conversation.d.ts.map +1 -0
  37. package/dist/conversation.js +29 -0
  38. package/dist/conversation.js.map +1 -0
  39. package/dist/db.d.ts +29 -0
  40. package/dist/db.d.ts.map +1 -0
  41. package/dist/db.js +205 -0
  42. package/dist/db.js.map +1 -0
  43. package/dist/discord.d.ts +17 -0
  44. package/dist/discord.d.ts.map +1 -0
  45. package/dist/discord.js +115 -0
  46. package/dist/discord.js.map +1 -0
  47. package/dist/env.d.ts +6 -0
  48. package/dist/env.d.ts.map +1 -0
  49. package/dist/env.js +36 -0
  50. package/dist/env.js.map +1 -0
  51. package/dist/gateway-client.d.ts +8 -0
  52. package/dist/gateway-client.d.ts.map +1 -0
  53. package/dist/gateway-client.js +57 -0
  54. package/dist/gateway-client.js.map +1 -0
  55. package/dist/gateway-service.d.ts +23 -0
  56. package/dist/gateway-service.d.ts.map +1 -0
  57. package/dist/gateway-service.js +360 -0
  58. package/dist/gateway-service.js.map +1 -0
  59. package/dist/gateway-types.d.ts +40 -0
  60. package/dist/gateway-types.d.ts.map +1 -0
  61. package/dist/gateway-types.js +6 -0
  62. package/dist/gateway-types.js.map +1 -0
  63. package/dist/gateway.d.ts +2 -0
  64. package/dist/gateway.d.ts.map +1 -0
  65. package/dist/gateway.js +138 -0
  66. package/dist/gateway.js.map +1 -0
  67. package/dist/hatch.d.ts +7 -0
  68. package/dist/hatch.d.ts.map +1 -0
  69. package/dist/hatch.js +99 -0
  70. package/dist/hatch.js.map +1 -0
  71. package/dist/health.d.ts +2 -0
  72. package/dist/health.d.ts.map +1 -0
  73. package/dist/health.js +169 -0
  74. package/dist/health.js.map +1 -0
  75. package/dist/heartbeat.d.ts +3 -0
  76. package/dist/heartbeat.d.ts.map +1 -0
  77. package/dist/heartbeat.js +103 -0
  78. package/dist/heartbeat.js.map +1 -0
  79. package/dist/hybridai-bots.d.ts +5 -0
  80. package/dist/hybridai-bots.d.ts.map +1 -0
  81. package/dist/hybridai-bots.js +34 -0
  82. package/dist/hybridai-bots.js.map +1 -0
  83. package/dist/index.d.ts +2 -0
  84. package/dist/index.d.ts.map +1 -0
  85. package/dist/index.js +60 -0
  86. package/dist/index.js.map +1 -0
  87. package/dist/ipc.d.ts +33 -0
  88. package/dist/ipc.d.ts.map +1 -0
  89. package/dist/ipc.js +142 -0
  90. package/dist/ipc.js.map +1 -0
  91. package/dist/logger.d.ts +3 -0
  92. package/dist/logger.d.ts.map +1 -0
  93. package/dist/logger.js +21 -0
  94. package/dist/logger.js.map +1 -0
  95. package/dist/mount-security.d.ts +28 -0
  96. package/dist/mount-security.d.ts.map +1 -0
  97. package/dist/mount-security.js +187 -0
  98. package/dist/mount-security.js.map +1 -0
  99. package/dist/onboarding.d.ts +7 -0
  100. package/dist/onboarding.d.ts.map +1 -0
  101. package/dist/onboarding.js +445 -0
  102. package/dist/onboarding.js.map +1 -0
  103. package/dist/prompt-hooks.d.ts +17 -0
  104. package/dist/prompt-hooks.d.ts.map +1 -0
  105. package/dist/prompt-hooks.js +83 -0
  106. package/dist/prompt-hooks.js.map +1 -0
  107. package/dist/runtime-config.d.ts +78 -0
  108. package/dist/runtime-config.d.ts.map +1 -0
  109. package/dist/runtime-config.js +471 -0
  110. package/dist/runtime-config.js.map +1 -0
  111. package/dist/scheduled-task-runner.d.ts +11 -0
  112. package/dist/scheduled-task-runner.d.ts.map +1 -0
  113. package/dist/scheduled-task-runner.js +16 -0
  114. package/dist/scheduled-task-runner.js.map +1 -0
  115. package/dist/scheduler.d.ts +11 -0
  116. package/dist/scheduler.d.ts.map +1 -0
  117. package/dist/scheduler.js +165 -0
  118. package/dist/scheduler.js.map +1 -0
  119. package/dist/session-maintenance.d.ts +9 -0
  120. package/dist/session-maintenance.d.ts.map +1 -0
  121. package/dist/session-maintenance.js +168 -0
  122. package/dist/session-maintenance.js.map +1 -0
  123. package/dist/session-transcripts.d.ts +11 -0
  124. package/dist/session-transcripts.d.ts.map +1 -0
  125. package/dist/session-transcripts.js +32 -0
  126. package/dist/session-transcripts.js.map +1 -0
  127. package/dist/side-effects.d.ts +3 -0
  128. package/dist/side-effects.d.ts.map +1 -0
  129. package/dist/side-effects.js +30 -0
  130. package/dist/side-effects.js.map +1 -0
  131. package/dist/skills.d.ts +32 -0
  132. package/dist/skills.d.ts.map +1 -0
  133. package/dist/skills.js +376 -0
  134. package/dist/skills.js.map +1 -0
  135. package/dist/tui.d.ts +2 -0
  136. package/dist/tui.d.ts.map +1 -0
  137. package/dist/tui.js +305 -0
  138. package/dist/tui.js.map +1 -0
  139. package/dist/types.d.ts +132 -0
  140. package/dist/types.d.ts.map +1 -0
  141. package/dist/types.js +3 -0
  142. package/dist/types.js.map +1 -0
  143. package/dist/workspace.d.ts +25 -0
  144. package/dist/workspace.d.ts.map +1 -0
  145. package/dist/workspace.js +154 -0
  146. package/dist/workspace.js.map +1 -0
  147. package/docs/chat.html +929 -0
  148. package/docs/hai_logo_free.png +0 -0
  149. package/docs/hero.png +0 -0
  150. package/docs/index.html +1213 -0
  151. package/package.json +34 -0
  152. package/skills/current-time/SKILL.md +26 -0
  153. package/skills/iss-position/SKILL.md +46 -0
  154. package/skills/iss-position/agents/openai.yaml +3 -0
  155. package/skills/iss-position/scripts/get_iss_position.py +107 -0
  156. package/skills/repo-orientation/SKILL.md +74 -0
  157. package/src/agent.ts +35 -0
  158. package/src/cli.ts +35 -0
  159. package/src/config.ts +137 -0
  160. package/src/container-runner.ts +305 -0
  161. package/src/conversation.ts +49 -0
  162. package/src/db.ts +290 -0
  163. package/src/discord.ts +156 -0
  164. package/src/env.ts +36 -0
  165. package/src/gateway-client.ts +73 -0
  166. package/src/gateway-service.ts +456 -0
  167. package/src/gateway-types.ts +47 -0
  168. package/src/gateway.ts +199 -0
  169. package/src/health.ts +189 -0
  170. package/src/heartbeat.ts +121 -0
  171. package/src/hybridai-bots.ts +48 -0
  172. package/src/ipc.ts +163 -0
  173. package/src/logger.ts +26 -0
  174. package/src/mount-security.ts +216 -0
  175. package/src/onboarding.ts +569 -0
  176. package/src/prompt-hooks.ts +113 -0
  177. package/src/runtime-config.ts +588 -0
  178. package/src/scheduled-task-runner.ts +26 -0
  179. package/src/scheduler.ts +196 -0
  180. package/src/session-maintenance.ts +263 -0
  181. package/src/session-transcripts.ts +45 -0
  182. package/src/side-effects.ts +41 -0
  183. package/src/skills.ts +429 -0
  184. package/src/tui.ts +327 -0
  185. package/src/types.ts +135 -0
  186. package/src/workspace.ts +171 -0
  187. package/templates/AGENTS.md +104 -0
  188. package/templates/BOOT.md +3 -0
  189. package/templates/BOOTSTRAP.md +54 -0
  190. package/templates/HEARTBEAT.md +5 -0
  191. package/templates/IDENTITY.md +18 -0
  192. package/templates/MEMORY.md +19 -0
  193. package/templates/SOUL.md +36 -0
  194. package/templates/TOOLS.md +22 -0
  195. package/templates/USER.md +17 -0
  196. package/tsconfig.json +18 -0
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@hybridaione/hybridclaw",
3
+ "version": "0.1.5",
4
+ "type": "module",
5
+ "description": "Personal AI assistant bot for Discord, powered by HybridAI",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "bin": {
10
+ "hybridclaw": "dist/cli.js"
11
+ },
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "lint": "tsc --noEmit --noUnusedLocals --noUnusedParameters",
15
+ "dev": "tsx src/cli.ts gateway",
16
+ "gateway": "tsx src/cli.ts gateway",
17
+ "tui": "tsx src/cli.ts tui",
18
+ "start": "node dist/cli.js gateway",
19
+ "build:container": "cd container && npm run build && docker build -t hybridclaw-agent ."
20
+ },
21
+ "dependencies": {
22
+ "better-sqlite3": "^11.8.1",
23
+ "cron-parser": "^5.5.0",
24
+ "discord.js": "^14.16.3",
25
+ "pino": "^9.6.0",
26
+ "pino-pretty": "^13.0.0"
27
+ },
28
+ "devDependencies": {
29
+ "@types/better-sqlite3": "^7.6.12",
30
+ "@types/node": "^22.10.0",
31
+ "tsx": "^4.19.0",
32
+ "typescript": "^5.7.0"
33
+ }
34
+ }
@@ -0,0 +1,26 @@
1
+ ---
2
+ name: current-time
3
+ description: Return the current system time (and timezone) accurately by calling a tool, not by guessing.
4
+ user-invocable: true
5
+ disable-model-invocation: false
6
+ ---
7
+
8
+ # Current Time
9
+
10
+ Use this skill when the user asks for the current time, date, timezone, "right now", "what time is it", or similar.
11
+
12
+ ## Workflow
13
+
14
+ 1. Run a real-time command with `bash`:
15
+ ```bash
16
+ date +"%Y-%m-%d %H:%M:%S %Z (%z)"
17
+ ```
18
+
19
+ 2. Return the result in one short line:
20
+ `Current time skill output: <output>`
21
+
22
+ ## Constraints
23
+
24
+ - Do not estimate or infer time from memory.
25
+ - Always use the command output from this turn.
26
+ - Keep the response concise unless the user asks for more detail.
@@ -0,0 +1,46 @@
1
+ ---
2
+ name: iss-position
3
+ description: Retrieve the current International Space Station (ISS) position from the WhereTheISS API and return standardized success/error payloads. Use when a user asks where the ISS is right now, requests current ISS latitude/longitude, or needs a reusable script to fetch live ISS coordinates.
4
+ ---
5
+
6
+ # ISS Position
7
+
8
+ Fetch the ISS latitude and longitude with a deterministic Python script that matches this repository's existing `get_iss_position` tool behavior.
9
+
10
+ ## Quick Start
11
+
12
+ 1. Run:
13
+ ```bash
14
+ python3 skills/iss-position/scripts/get_iss_position.py --format text
15
+ ```
16
+ 2. For machine-readable output, run:
17
+ ```bash
18
+ python3 skills/iss-position/scripts/get_iss_position.py --format json
19
+ ```
20
+
21
+ ## Script Contract
22
+
23
+ Script path: `skills/iss-position/scripts/get_iss_position.py`
24
+
25
+ Behavior:
26
+ 1. Call `https://api.wheretheiss.at/v1/satellites/25544`.
27
+ 2. Use a default timeout of 15 seconds (override with `--timeout`).
28
+ 3. Validate `latitude` and `longitude` as numeric values.
29
+ 4. Return either:
30
+ ```json
31
+ {"status":"success","latitude":12.34,"longitude":56.78,"message":"The ISS is currently at latitude 12.34 and longitude 56.78."}
32
+ ```
33
+ or:
34
+ ```json
35
+ {"status":"error","message":"Failed to retrieve ISS position."}
36
+ ```
37
+ or:
38
+ ```json
39
+ {"status":"error","message":"ISS API returned invalid latitude/longitude values"}
40
+ ```
41
+
42
+ ## Notes
43
+
44
+ - Network access is required to fetch live ISS coordinates.
45
+ - Use `--format text` for human-readable output and `--format json` for downstream tooling.
46
+ - Use `--timeout` to adjust request timeout when needed.
@@ -0,0 +1,3 @@
1
+ interface:
2
+ display_name: "ISS Position"
3
+ short_description: "Fetch live ISS latitude and longitude"
@@ -0,0 +1,107 @@
1
+ # ruff: noqa: INP001
2
+ """Fetch the current ISS position and print a tool-compatible response."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import sys
9
+ import urllib.request
10
+ import urllib.error
11
+ from typing import Any, TypedDict
12
+
13
+ API_URL = "https://api.wheretheiss.at/v1/satellites/25544"
14
+ DEFAULT_REQUEST_TIMEOUT = 15
15
+ REQUEST_FAILURE_MESSAGE = "Failed to retrieve ISS position."
16
+ INVALID_COORDS_MESSAGE = "ISS API returned invalid latitude/longitude values"
17
+
18
+
19
+ class IssPosition(TypedDict):
20
+ latitude: float
21
+ longitude: float
22
+
23
+
24
+ class IssApiError(RuntimeError):
25
+ """Raised when the ISS API request fails or returns invalid data."""
26
+
27
+
28
+ def fetch_current_position(timeout: int = DEFAULT_REQUEST_TIMEOUT) -> IssPosition:
29
+ """Fetch current ISS coordinates from WhereTheISS."""
30
+
31
+ try:
32
+ with urllib.request.urlopen(API_URL, timeout=timeout) as response:
33
+ payload: dict[str, Any] = json.loads(response.read())
34
+ except (urllib.error.URLError, OSError) as exc:
35
+ raise IssApiError(REQUEST_FAILURE_MESSAGE) from exc
36
+
37
+ latitude = payload.get("latitude")
38
+ longitude = payload.get("longitude")
39
+ if not isinstance(latitude, (int, float)) or not isinstance(
40
+ longitude, (int, float)
41
+ ):
42
+ raise IssApiError(INVALID_COORDS_MESSAGE)
43
+
44
+ return IssPosition(latitude=float(latitude), longitude=float(longitude))
45
+
46
+
47
+ def _success_response(position: IssPosition) -> dict[str, Any]:
48
+ latitude = position["latitude"]
49
+ longitude = position["longitude"]
50
+ return {
51
+ "status": "success",
52
+ "latitude": latitude,
53
+ "longitude": longitude,
54
+ "message": f"The ISS is currently at latitude {latitude} and longitude {longitude}.",
55
+ }
56
+
57
+
58
+ def _error_response(message: str) -> dict[str, str]:
59
+ return {"status": "error", "message": message}
60
+
61
+
62
+ def _parse_args() -> argparse.Namespace:
63
+ parser = argparse.ArgumentParser(description="Fetch the current ISS position.")
64
+ parser.add_argument(
65
+ "--format",
66
+ choices=("text", "json"),
67
+ default="json",
68
+ help="Output format. Use 'json' for tools and 'text' for humans.",
69
+ )
70
+ parser.add_argument(
71
+ "--timeout",
72
+ type=int,
73
+ default=DEFAULT_REQUEST_TIMEOUT,
74
+ help=f"Request timeout in seconds (default: {DEFAULT_REQUEST_TIMEOUT}).",
75
+ )
76
+ return parser.parse_args()
77
+
78
+
79
+ def _emit(payload: dict[str, Any], output_format: str) -> None:
80
+ if output_format == "json":
81
+ print(json.dumps(payload, ensure_ascii=True))
82
+ return
83
+
84
+ status = payload.get("status")
85
+ if status == "success":
86
+ print(payload["message"])
87
+ return
88
+
89
+ print(f"Error: {payload.get('message', 'Unknown error')}", file=sys.stderr)
90
+
91
+
92
+ def main() -> int:
93
+ args = _parse_args()
94
+
95
+ try:
96
+ payload = _success_response(fetch_current_position(timeout=args.timeout))
97
+ except IssApiError as exc:
98
+ payload = _error_response(str(exc))
99
+ _emit(payload, args.format)
100
+ return 1
101
+
102
+ _emit(payload, args.format)
103
+ return 0
104
+
105
+
106
+ if __name__ == "__main__":
107
+ raise SystemExit(main())
@@ -0,0 +1,74 @@
1
+ ---
2
+ name: repo-orientation
3
+ description: Quickly map an unfamiliar repository and identify where a requested feature should be implemented.
4
+ user-invocable: true
5
+ disable-model-invocation: false
6
+ ---
7
+
8
+ # Repo Orientation
9
+
10
+ Use this skill when the user asks where code lives, how a feature works, or where to make a change in an unfamiliar repo.
11
+
12
+ ## Tooling Notes
13
+
14
+ - Prefer `glob` + `grep` first. They are cheaper and more reliable than broad shell exploration.
15
+ - Use `read` sparingly for small/high-value files only.
16
+ - Keep the whole run under about 8 tool calls.
17
+
18
+ ## Workflow
19
+
20
+ 1. Establish top-level structure with `glob`.
21
+ Run:
22
+ ```json
23
+ {"pattern":"*"}
24
+ ```
25
+
26
+ 2. Build a focused file map with `glob`.
27
+ Run one or two targeted globs, not many:
28
+ ```json
29
+ {"pattern":"src/**/*"}
30
+ {"pattern":"test/**/*"}
31
+ {"pattern":"docs/**/*"}
32
+ ```
33
+
34
+ 3. Find likely implementation points by keyword with `grep`.
35
+ Run:
36
+ ```json
37
+ {"pattern":"<keyword|feature|command|route|tool>","path":"src"}
38
+ ```
39
+ If needed, repeat for one extra directory (`test` or `docs`).
40
+
41
+ 4. Read only the most relevant files (max 3 reads).
42
+ Prioritize:
43
+ - entrypoints (`src/index.*`, `src/main.*`, `src/cli.*`)
44
+ - docs (`README.md`, `CLAUDE.md`, `AGENTS.md`)
45
+ - files returned by search in step 3
46
+
47
+ 5. Return a concise map immediately.
48
+ Include:
49
+ - probable files to edit
50
+ - related files to verify
51
+ - risks/regressions to check
52
+
53
+ ## Output Template
54
+
55
+ Use this exact structure:
56
+
57
+ ```markdown
58
+ Feature map:
59
+ - Primary file(s):
60
+ - Supporting file(s):
61
+ - Tests to update:
62
+
63
+ Why these files:
64
+ - ...
65
+
66
+ Proposed first edit:
67
+ - ...
68
+ ```
69
+
70
+ ## Constraints
71
+
72
+ - Do not keep scanning once you have a plausible file map.
73
+ - Do not read large unrelated files "just in case."
74
+ - If confidence is low, state assumptions and list the next command to disambiguate.
package/src/agent.ts ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Agent — always runs through a container for consistent sandboxing.
3
+ */
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+
7
+ import { DATA_DIR } from './config.js';
8
+ import { runContainer } from './container-runner.js';
9
+ import type { ChatMessage, ContainerOutput, ScheduledTask, ToolProgressEvent } from './types.js';
10
+
11
+ /** Write full prompt context to data/last_prompt.jsonl for debugging (Pi-Mono style). */
12
+ function dumpPrompt(sessionId: string, messages: ChatMessage[], model: string, chatbotId: string): void {
13
+ try {
14
+ const entry = { ts: new Date().toISOString(), sessionId, model, chatbotId, messages };
15
+ const filePath = path.join(DATA_DIR, 'last_prompt.jsonl');
16
+ fs.writeFileSync(filePath, JSON.stringify(entry) + '\n');
17
+ } catch { /* best-effort */ }
18
+ }
19
+
20
+ export async function runAgent(
21
+ sessionId: string,
22
+ messages: ChatMessage[],
23
+ chatbotId: string,
24
+ enableRag: boolean,
25
+ model: string,
26
+ agentId: string,
27
+ channelId: string,
28
+ scheduledTasks?: ScheduledTask[],
29
+ allowedTools?: string[],
30
+ onToolProgress?: (event: ToolProgressEvent) => void,
31
+ abortSignal?: AbortSignal,
32
+ ): Promise<ContainerOutput> {
33
+ dumpPrompt(sessionId, messages, model, chatbotId);
34
+ return runContainer(sessionId, messages, chatbotId, enableRag, model, agentId, channelId, scheduledTasks, allowedTools, onToolProgress, abortSignal);
35
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { ensureHybridAICredentials } from './onboarding.js';
4
+
5
+ async function main(): Promise<void> {
6
+ const command = process.argv[2];
7
+
8
+ switch (command) {
9
+ case 'gateway':
10
+ await ensureHybridAICredentials({ commandName: 'hybridclaw gateway' });
11
+ await import('./gateway.js');
12
+ break;
13
+ case 'tui':
14
+ await ensureHybridAICredentials({ commandName: 'hybridclaw tui' });
15
+ await import('./tui.js');
16
+ break;
17
+ case 'onboarding':
18
+ await ensureHybridAICredentials({ force: true, commandName: 'hybridclaw onboarding' });
19
+ break;
20
+ default:
21
+ console.log(`Usage: hybridclaw <command>
22
+
23
+ Commands:
24
+ gateway Start core runtime (web/API/scheduler/heartbeat/optional Discord)
25
+ tui Start terminal adapter (requires running gateway)
26
+ onboarding Run HybridAI account/API key onboarding`);
27
+ process.exit(command ? 1 : 0);
28
+ }
29
+ }
30
+
31
+ main().catch((err) => {
32
+ const message = err instanceof Error ? err.message : String(err);
33
+ console.error(`hybridclaw error: ${message}`);
34
+ process.exit(1);
35
+ });
package/src/config.ts ADDED
@@ -0,0 +1,137 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+
5
+ import { loadEnvFile } from './env.js';
6
+ import {
7
+ ensureRuntimeConfigFile,
8
+ getRuntimeConfig,
9
+ onRuntimeConfigChange,
10
+ type RuntimeConfig,
11
+ } from './runtime-config.js';
12
+
13
+ loadEnvFile();
14
+ ensureRuntimeConfigFile();
15
+
16
+ function required(name: string): string {
17
+ const val = process.env[name];
18
+ if (!val) throw new Error(`Missing required env var: ${name}`);
19
+ return val;
20
+ }
21
+
22
+ function resolveAppVersion(): string {
23
+ const envVersion = process.env.npm_package_version;
24
+ if (envVersion) return envVersion;
25
+
26
+ const packagePath = path.join(process.cwd(), 'package.json');
27
+ try {
28
+ const raw = fs.readFileSync(packagePath, 'utf-8');
29
+ const parsed = JSON.parse(raw) as { version?: unknown };
30
+ if (typeof parsed.version === 'string' && parsed.version.trim()) {
31
+ return parsed.version.trim();
32
+ }
33
+ } catch {
34
+ // fall through
35
+ }
36
+
37
+ return '0.0.0';
38
+ }
39
+
40
+ export const APP_VERSION = resolveAppVersion();
41
+
42
+ // Secrets stay in env/.env
43
+ export const DISCORD_TOKEN = process.env.DISCORD_TOKEN || '';
44
+ export const HYBRIDAI_API_KEY = required('HYBRIDAI_API_KEY');
45
+
46
+ // Runtime settings hot-reload from config.json
47
+ export let DISCORD_PREFIX = '!claw';
48
+
49
+ export let HYBRIDAI_BASE_URL = 'https://hybridai.one';
50
+ export let HYBRIDAI_MODEL = 'gpt-5-nano';
51
+ export let HYBRIDAI_CHATBOT_ID = '';
52
+ export let HYBRIDAI_ENABLE_RAG = true;
53
+ export let HYBRIDAI_MODELS: string[] = ['gpt-5-nano', 'gpt-5-mini', 'gpt-5'];
54
+
55
+ export let CONTAINER_IMAGE = 'hybridclaw-agent';
56
+ export let CONTAINER_MEMORY = '512m';
57
+ export let CONTAINER_CPUS = '1';
58
+ export let CONTAINER_TIMEOUT = 60_000;
59
+
60
+ export const MOUNT_ALLOWLIST_PATH = path.join(
61
+ os.homedir(), '.config', 'hybridclaw', 'mount-allowlist.json',
62
+ );
63
+ export let ADDITIONAL_MOUNTS = '';
64
+
65
+ export let CONTAINER_MAX_OUTPUT_SIZE = 10_485_760;
66
+ export let MAX_CONCURRENT_CONTAINERS = 5;
67
+
68
+ export let HEARTBEAT_ENABLED = true;
69
+ export let HEARTBEAT_INTERVAL = 1_800_000;
70
+ export let HEARTBEAT_CHANNEL = '';
71
+
72
+ export let HEALTH_HOST = '127.0.0.1';
73
+ export let HEALTH_PORT = 9090;
74
+ export let WEB_API_TOKEN = '';
75
+ export let GATEWAY_BASE_URL = 'http://127.0.0.1:9090';
76
+ export let GATEWAY_API_TOKEN = '';
77
+ export let DB_PATH = 'data/hybridclaw.db';
78
+ export let DATA_DIR = path.dirname(DB_PATH);
79
+
80
+ export let SESSION_COMPACTION_ENABLED = true;
81
+ export let SESSION_COMPACTION_THRESHOLD = 120;
82
+ export let SESSION_COMPACTION_KEEP_RECENT = 40;
83
+ export let SESSION_COMPACTION_SUMMARY_MAX_CHARS = 8_000;
84
+ export let PRE_COMPACTION_MEMORY_FLUSH_ENABLED = true;
85
+ export let PRE_COMPACTION_MEMORY_FLUSH_MAX_MESSAGES = 80;
86
+ export let PRE_COMPACTION_MEMORY_FLUSH_MAX_CHARS = 24_000;
87
+
88
+ function applyRuntimeConfig(config: RuntimeConfig): void {
89
+ DISCORD_PREFIX = config.discord.prefix;
90
+
91
+ HYBRIDAI_BASE_URL = config.hybridai.baseUrl;
92
+ HYBRIDAI_MODEL = config.hybridai.defaultModel;
93
+ HYBRIDAI_CHATBOT_ID = config.hybridai.defaultChatbotId;
94
+ HYBRIDAI_ENABLE_RAG = config.hybridai.enableRag;
95
+ HYBRIDAI_MODELS = [...config.hybridai.models];
96
+
97
+ CONTAINER_IMAGE = config.container.image;
98
+ CONTAINER_MEMORY = config.container.memory;
99
+ CONTAINER_CPUS = config.container.cpus;
100
+ CONTAINER_TIMEOUT = config.container.timeoutMs;
101
+ ADDITIONAL_MOUNTS = config.container.additionalMounts;
102
+ CONTAINER_MAX_OUTPUT_SIZE = config.container.maxOutputBytes;
103
+ MAX_CONCURRENT_CONTAINERS = Math.max(1, config.container.maxConcurrent);
104
+
105
+ HEARTBEAT_ENABLED = config.heartbeat.enabled;
106
+ HEARTBEAT_INTERVAL = config.heartbeat.intervalMs;
107
+ HEARTBEAT_CHANNEL = config.heartbeat.channel;
108
+
109
+ HEALTH_HOST = config.ops.healthHost;
110
+ HEALTH_PORT = config.ops.healthPort;
111
+ WEB_API_TOKEN = process.env.WEB_API_TOKEN || config.ops.webApiToken;
112
+ GATEWAY_BASE_URL = config.ops.gatewayBaseUrl;
113
+ GATEWAY_API_TOKEN = process.env.GATEWAY_API_TOKEN || config.ops.gatewayApiToken || WEB_API_TOKEN;
114
+ DB_PATH = config.ops.dbPath;
115
+ DATA_DIR = path.dirname(DB_PATH);
116
+
117
+ SESSION_COMPACTION_ENABLED = config.sessionCompaction.enabled;
118
+ SESSION_COMPACTION_THRESHOLD = Math.max(20, config.sessionCompaction.threshold);
119
+ SESSION_COMPACTION_KEEP_RECENT = Math.max(
120
+ 1,
121
+ Math.min(config.sessionCompaction.keepRecent, SESSION_COMPACTION_THRESHOLD - 1),
122
+ );
123
+ SESSION_COMPACTION_SUMMARY_MAX_CHARS = Math.max(1_000, config.sessionCompaction.summaryMaxChars);
124
+ PRE_COMPACTION_MEMORY_FLUSH_ENABLED = config.sessionCompaction.preCompactionMemoryFlush.enabled;
125
+ PRE_COMPACTION_MEMORY_FLUSH_MAX_MESSAGES = Math.max(8, config.sessionCompaction.preCompactionMemoryFlush.maxMessages);
126
+ PRE_COMPACTION_MEMORY_FLUSH_MAX_CHARS = Math.max(4_000, config.sessionCompaction.preCompactionMemoryFlush.maxChars);
127
+ }
128
+
129
+ applyRuntimeConfig(getRuntimeConfig());
130
+ onRuntimeConfigChange((next) => {
131
+ applyRuntimeConfig(next);
132
+ });
133
+
134
+ export { onRuntimeConfigChange as onConfigChange };
135
+ export function getConfigSnapshot(): RuntimeConfig {
136
+ return getRuntimeConfig();
137
+ }