@hybridaione/hybridclaw 0.2.3 → 0.2.7
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 +70 -0
- package/.husky/pre-commit +1 -0
- package/CHANGELOG.md +95 -0
- package/CONTRIBUTING.md +33 -0
- package/README.md +57 -19
- package/SECURITY.md +17 -0
- package/biome.json +35 -0
- package/config.example.json +72 -7
- package/container/package-lock.json +2 -2
- package/container/package.json +1 -1
- package/container/src/approval-policy.ts +1405 -0
- package/container/src/browser-tools.ts +672 -136
- package/container/src/extensions.ts +36 -12
- package/container/src/hybridai-client.ts +65 -16
- package/container/src/index.ts +463 -110
- package/container/src/ipc.ts +5 -3
- package/container/src/token-usage.ts +107 -18
- package/container/src/tools.ts +600 -225
- package/container/src/types.ts +49 -2
- package/container/src/web-fetch.ts +89 -32
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +15 -4
- package/dist/agent.js.map +1 -1
- package/dist/audit-cli.d.ts.map +1 -1
- package/dist/audit-cli.js +4 -2
- package/dist/audit-cli.js.map +1 -1
- package/dist/audit-events.d.ts.map +1 -1
- package/dist/audit-events.js +53 -3
- package/dist/audit-events.js.map +1 -1
- package/dist/audit-trail.d.ts.map +1 -1
- package/dist/audit-trail.js +17 -8
- package/dist/audit-trail.js.map +1 -1
- package/dist/channels/discord/attachments.d.ts.map +1 -1
- package/dist/channels/discord/attachments.js +14 -7
- package/dist/channels/discord/attachments.js.map +1 -1
- package/dist/channels/discord/debounce.d.ts +9 -0
- package/dist/channels/discord/debounce.d.ts.map +1 -0
- package/dist/channels/discord/debounce.js +20 -0
- package/dist/channels/discord/debounce.js.map +1 -0
- package/dist/channels/discord/delivery.d.ts +4 -1
- package/dist/channels/discord/delivery.d.ts.map +1 -1
- package/dist/channels/discord/delivery.js +24 -4
- package/dist/channels/discord/delivery.js.map +1 -1
- package/dist/channels/discord/human-delay.d.ts +16 -0
- package/dist/channels/discord/human-delay.d.ts.map +1 -0
- package/dist/channels/discord/human-delay.js +29 -0
- package/dist/channels/discord/human-delay.js.map +1 -0
- package/dist/channels/discord/inbound.d.ts +31 -0
- package/dist/channels/discord/inbound.d.ts.map +1 -1
- package/dist/channels/discord/inbound.js +156 -6
- package/dist/channels/discord/inbound.js.map +1 -1
- package/dist/channels/discord/mentions.d.ts.map +1 -1
- package/dist/channels/discord/mentions.js +16 -4
- package/dist/channels/discord/mentions.js.map +1 -1
- package/dist/channels/discord/presence.d.ts +33 -0
- package/dist/channels/discord/presence.d.ts.map +1 -0
- package/dist/channels/discord/presence.js +111 -0
- package/dist/channels/discord/presence.js.map +1 -0
- package/dist/channels/discord/rate-limiter.d.ts +14 -0
- package/dist/channels/discord/rate-limiter.d.ts.map +1 -0
- package/dist/channels/discord/rate-limiter.js +49 -0
- package/dist/channels/discord/rate-limiter.js.map +1 -0
- package/dist/channels/discord/reactions.d.ts +38 -0
- package/dist/channels/discord/reactions.d.ts.map +1 -0
- package/dist/channels/discord/reactions.js +151 -0
- package/dist/channels/discord/reactions.js.map +1 -0
- package/dist/channels/discord/runtime.d.ts +7 -4
- package/dist/channels/discord/runtime.d.ts.map +1 -1
- package/dist/channels/discord/runtime.js +755 -150
- package/dist/channels/discord/runtime.js.map +1 -1
- package/dist/channels/discord/stream.d.ts +4 -1
- package/dist/channels/discord/stream.d.ts.map +1 -1
- package/dist/channels/discord/stream.js +19 -12
- package/dist/channels/discord/stream.js.map +1 -1
- package/dist/channels/discord/tool-actions.d.ts.map +1 -1
- package/dist/channels/discord/tool-actions.js +24 -12
- package/dist/channels/discord/tool-actions.js.map +1 -1
- package/dist/channels/discord/typing.d.ts +15 -0
- package/dist/channels/discord/typing.d.ts.map +1 -0
- package/dist/channels/discord/typing.js +106 -0
- package/dist/channels/discord/typing.js.map +1 -0
- package/dist/chunk.d.ts.map +1 -1
- package/dist/chunk.js +4 -2
- package/dist/chunk.js.map +1 -1
- package/dist/cli.js +77 -23
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +24 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +113 -18
- package/dist/config.js.map +1 -1
- package/dist/container-runner.d.ts.map +1 -1
- package/dist/container-runner.js +59 -26
- package/dist/container-runner.js.map +1 -1
- package/dist/container-setup.d.ts.map +1 -1
- package/dist/container-setup.js +10 -9
- package/dist/container-setup.js.map +1 -1
- package/dist/conversation.d.ts +2 -2
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +1 -1
- package/dist/conversation.js.map +1 -1
- package/dist/db.d.ts +118 -2
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +1568 -50
- package/dist/db.js.map +1 -1
- package/dist/delegation-manager.d.ts.map +1 -1
- package/dist/delegation-manager.js +3 -2
- package/dist/delegation-manager.js.map +1 -1
- package/dist/gateway-client.d.ts +2 -2
- package/dist/gateway-client.d.ts.map +1 -1
- package/dist/gateway-client.js +10 -4
- package/dist/gateway-client.js.map +1 -1
- package/dist/gateway-service.d.ts +3 -3
- package/dist/gateway-service.d.ts.map +1 -1
- package/dist/gateway-service.js +624 -87
- package/dist/gateway-service.js.map +1 -1
- package/dist/gateway-types.d.ts +24 -0
- package/dist/gateway-types.d.ts.map +1 -1
- package/dist/gateway-types.js.map +1 -1
- package/dist/gateway.js +303 -26
- package/dist/gateway.js.map +1 -1
- package/dist/health.d.ts.map +1 -1
- package/dist/health.js +20 -10
- package/dist/health.js.map +1 -1
- package/dist/heartbeat.d.ts +4 -0
- package/dist/heartbeat.d.ts.map +1 -1
- package/dist/heartbeat.js +62 -20
- package/dist/heartbeat.js.map +1 -1
- package/dist/hybridai-bots.d.ts.map +1 -1
- package/dist/hybridai-bots.js +4 -2
- package/dist/hybridai-bots.js.map +1 -1
- package/dist/hybridai-models.d.ts +8 -0
- package/dist/hybridai-models.d.ts.map +1 -0
- package/dist/hybridai-models.js +94 -0
- package/dist/hybridai-models.js.map +1 -0
- package/dist/instruction-approval-audit.d.ts.map +1 -1
- package/dist/instruction-approval-audit.js.map +1 -1
- package/dist/instruction-integrity.d.ts.map +1 -1
- package/dist/instruction-integrity.js +8 -2
- package/dist/instruction-integrity.js.map +1 -1
- package/dist/ipc.d.ts.map +1 -1
- package/dist/ipc.js +6 -1
- package/dist/ipc.js.map +1 -1
- package/dist/logger.js.map +1 -1
- package/dist/memory-consolidation.d.ts +17 -0
- package/dist/memory-consolidation.d.ts.map +1 -0
- package/dist/memory-consolidation.js +25 -0
- package/dist/memory-consolidation.js.map +1 -0
- package/dist/memory-service.d.ts +200 -0
- package/dist/memory-service.d.ts.map +1 -0
- package/dist/memory-service.js +294 -0
- package/dist/memory-service.js.map +1 -0
- package/dist/mount-security.d.ts.map +1 -1
- package/dist/mount-security.js +31 -7
- package/dist/mount-security.js.map +1 -1
- package/dist/observability-ingest.d.ts.map +1 -1
- package/dist/observability-ingest.js +32 -11
- package/dist/observability-ingest.js.map +1 -1
- package/dist/onboarding.d.ts.map +1 -1
- package/dist/onboarding.js +32 -9
- package/dist/onboarding.js.map +1 -1
- package/dist/proactive-policy.d.ts.map +1 -1
- package/dist/proactive-policy.js +2 -1
- package/dist/proactive-policy.js.map +1 -1
- package/dist/prompt-hooks.d.ts.map +1 -1
- package/dist/prompt-hooks.js +11 -7
- package/dist/prompt-hooks.js.map +1 -1
- package/dist/runtime-config.d.ts +104 -1
- package/dist/runtime-config.d.ts.map +1 -1
- package/dist/runtime-config.js +503 -24
- package/dist/runtime-config.js.map +1 -1
- package/dist/scheduled-task-runner.d.ts +1 -0
- package/dist/scheduled-task-runner.d.ts.map +1 -1
- package/dist/scheduled-task-runner.js +43 -10
- package/dist/scheduled-task-runner.js.map +1 -1
- package/dist/scheduler.d.ts +43 -4
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +530 -56
- package/dist/scheduler.js.map +1 -1
- package/dist/session-export.d.ts +26 -0
- package/dist/session-export.d.ts.map +1 -0
- package/dist/session-export.js +149 -0
- package/dist/session-export.js.map +1 -0
- package/dist/session-maintenance.d.ts.map +1 -1
- package/dist/session-maintenance.js +75 -13
- package/dist/session-maintenance.js.map +1 -1
- package/dist/session-transcripts.d.ts.map +1 -1
- package/dist/session-transcripts.js.map +1 -1
- package/dist/side-effects.d.ts.map +1 -1
- package/dist/side-effects.js +14 -2
- package/dist/side-effects.js.map +1 -1
- package/dist/skills-guard.d.ts.map +1 -1
- package/dist/skills-guard.js +893 -130
- package/dist/skills-guard.js.map +1 -1
- package/dist/skills.d.ts +5 -0
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +29 -15
- package/dist/skills.js.map +1 -1
- package/dist/token-efficiency.d.ts.map +1 -1
- package/dist/token-efficiency.js.map +1 -1
- package/dist/tui.js +92 -11
- package/dist/tui.js.map +1 -1
- package/dist/types.d.ts +150 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +24 -1
- package/dist/types.js.map +1 -1
- package/dist/update.d.ts.map +1 -1
- package/dist/update.js +42 -14
- package/dist/update.js.map +1 -1
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +49 -9
- package/dist/workspace.js.map +1 -1
- package/docs/chat.html +9 -3
- package/docs/index.html +72 -13
- package/package.json +8 -2
- package/src/agent.ts +31 -4
- package/src/audit-cli.ts +44 -16
- package/src/audit-events.ts +69 -5
- package/src/audit-trail.ts +41 -15
- package/src/channels/discord/attachments.ts +81 -27
- package/src/channels/discord/debounce.ts +25 -0
- package/src/channels/discord/delivery.ts +65 -14
- package/src/channels/discord/human-delay.ts +48 -0
- package/src/channels/discord/inbound.ts +190 -8
- package/src/channels/discord/mentions.ts +42 -18
- package/src/channels/discord/presence.ts +148 -0
- package/src/channels/discord/rate-limiter.ts +58 -0
- package/src/channels/discord/reactions.ts +211 -0
- package/src/channels/discord/runtime.ts +1161 -205
- package/src/channels/discord/stream.ts +85 -31
- package/src/channels/discord/tool-actions.ts +78 -37
- package/src/channels/discord/typing.ts +140 -0
- package/src/chunk.ts +12 -4
- package/src/cli.ts +174 -57
- package/src/config.ts +208 -36
- package/src/container-runner.ts +134 -42
- package/src/container-setup.ts +57 -22
- package/src/conversation.ts +9 -7
- package/src/db.ts +2217 -84
- package/src/delegation-manager.ts +6 -2
- package/src/gateway-client.ts +41 -17
- package/src/gateway-service.ts +996 -220
- package/src/gateway-types.ts +33 -0
- package/src/gateway.ts +487 -52
- package/src/health.ts +66 -26
- package/src/heartbeat.ts +99 -22
- package/src/hybridai-bots.ts +14 -5
- package/src/hybridai-models.ts +158 -0
- package/src/instruction-approval-audit.ts +4 -1
- package/src/instruction-integrity.ts +30 -9
- package/src/ipc.ts +23 -5
- package/src/logger.ts +4 -1
- package/src/memory-consolidation.ts +41 -0
- package/src/memory-service.ts +606 -0
- package/src/mount-security.ts +58 -13
- package/src/observability-ingest.ts +134 -35
- package/src/onboarding.ts +126 -35
- package/src/proactive-policy.ts +3 -1
- package/src/prompt-hooks.ts +42 -17
- package/src/runtime-config.ts +1112 -122
- package/src/scheduled-task-runner.ts +77 -11
- package/src/scheduler.ts +683 -60
- package/src/session-export.ts +196 -0
- package/src/session-maintenance.ts +125 -22
- package/src/session-transcripts.ts +12 -3
- package/src/side-effects.ts +28 -5
- package/src/skills-guard.ts +1067 -219
- package/src/skills.ts +163 -65
- package/src/token-efficiency.ts +31 -9
- package/src/tui.ts +166 -25
- package/src/types.ts +199 -2
- package/src/update.ts +79 -23
- package/src/workspace.ts +63 -11
- package/tests/approval-policy.test.ts +335 -0
- package/tests/discord.basic.test.ts +213 -4
- package/tests/discord.human-presence.test.ts +85 -0
- package/tests/gateway-service.media-routing.test.ts +8 -2
- package/tests/hybridai-client.test.ts +112 -0
- package/tests/hybridai-models.test.ts +46 -0
- package/tests/memory-service.test.ts +1114 -0
- package/tests/token-efficiency.basic.test.ts +8 -2
- package/tests/token-usage.cache.test.ts +128 -0
- package/vitest.e2e.config.ts +3 -1
- package/vitest.integration.config.ts +3 -1
- package/vitest.live.config.ts +3 -1
- package/vitest.unit.config.ts +9 -0
package/src/cli.ts
CHANGED
|
@@ -1,20 +1,59 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { spawn, spawnSync } from 'child_process';
|
|
3
4
|
import fs from 'fs';
|
|
4
5
|
import path from 'path';
|
|
5
|
-
import { spawn, spawnSync } from 'child_process';
|
|
6
6
|
import readline from 'readline/promises';
|
|
7
|
-
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import {
|
|
9
|
+
APP_VERSION,
|
|
10
|
+
DATA_DIR,
|
|
11
|
+
GATEWAY_BASE_URL,
|
|
12
|
+
MissingRequiredEnvVarError,
|
|
13
|
+
} from './config.js';
|
|
8
14
|
import { ensureHybridAICredentials } from './onboarding.js';
|
|
9
|
-
import { APP_VERSION, DATA_DIR, GATEWAY_BASE_URL, MissingRequiredEnvVarError } from './config.js';
|
|
10
15
|
import { printUpdateUsage, runUpdateCommand } from './update.js';
|
|
11
16
|
|
|
12
|
-
|
|
17
|
+
const PACKAGE_NAME = '@hybridaione/hybridclaw';
|
|
18
|
+
let cachedInstallRoot: string | null = null;
|
|
19
|
+
|
|
20
|
+
function resolveInstallRoot(): string {
|
|
21
|
+
if (cachedInstallRoot) return cachedInstallRoot;
|
|
22
|
+
let current = path.dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
|
|
24
|
+
while (true) {
|
|
25
|
+
const packageJsonPath = path.join(current, 'package.json');
|
|
26
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
27
|
+
try {
|
|
28
|
+
const parsed = JSON.parse(
|
|
29
|
+
fs.readFileSync(packageJsonPath, 'utf-8'),
|
|
30
|
+
) as Partial<{ name: string }>;
|
|
31
|
+
if (parsed.name === PACKAGE_NAME) {
|
|
32
|
+
cachedInstallRoot = current;
|
|
33
|
+
return cachedInstallRoot;
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
// ignore parse errors and continue searching upward
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const parent = path.dirname(current);
|
|
40
|
+
if (parent === current) break;
|
|
41
|
+
current = parent;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
cachedInstallRoot = process.cwd();
|
|
45
|
+
return cachedInstallRoot;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function ensureRuntimeContainer(
|
|
49
|
+
commandName: string,
|
|
50
|
+
required = true,
|
|
51
|
+
): Promise<void> {
|
|
13
52
|
const { ensureContainerImageReady } = await import('./container-setup.js');
|
|
14
53
|
await ensureContainerImageReady({
|
|
15
54
|
commandName,
|
|
16
55
|
required,
|
|
17
|
-
cwd:
|
|
56
|
+
cwd: resolveInstallRoot(),
|
|
18
57
|
});
|
|
19
58
|
}
|
|
20
59
|
|
|
@@ -43,13 +82,15 @@ async function ensureGatewayForTui(commandName: string): Promise<void> {
|
|
|
43
82
|
return;
|
|
44
83
|
}
|
|
45
84
|
|
|
46
|
-
console.log(
|
|
85
|
+
console.log(
|
|
86
|
+
`${commandName}: Gateway not found. Starting gateway backend at ${GATEWAY_BASE_URL}.`,
|
|
87
|
+
);
|
|
47
88
|
await startGatewayBackend(commandName, true);
|
|
48
89
|
|
|
49
90
|
if (!(await isGatewayReachable())) {
|
|
50
91
|
throw new Error(
|
|
51
|
-
`Gateway did not become available at ${GATEWAY_BASE_URL} after startup.`
|
|
52
|
-
|
|
92
|
+
`Gateway did not become available at ${GATEWAY_BASE_URL} after startup.` +
|
|
93
|
+
' Please run `hybridclaw gateway start --foreground` in another terminal and try again.',
|
|
53
94
|
);
|
|
54
95
|
}
|
|
55
96
|
}
|
|
@@ -81,17 +122,17 @@ function formatInstructionDiffLine(file: {
|
|
|
81
122
|
];
|
|
82
123
|
}
|
|
83
124
|
|
|
84
|
-
async function ensureTuiInstructionApproval(
|
|
125
|
+
async function ensureTuiInstructionApproval(
|
|
126
|
+
commandName: string,
|
|
127
|
+
): Promise<void> {
|
|
85
128
|
const {
|
|
86
129
|
approveInstructionBaseline,
|
|
87
130
|
INSTRUCTION_BASELINE_PATH,
|
|
88
131
|
summarizeInstructionIntegrity,
|
|
89
132
|
verifyInstructionBaseline,
|
|
90
133
|
} = await import('./instruction-integrity.js');
|
|
91
|
-
const {
|
|
92
|
-
|
|
93
|
-
completeInstructionApprovalAudit,
|
|
94
|
-
} = await import('./instruction-approval-audit.js');
|
|
134
|
+
const { beginInstructionApprovalAudit, completeInstructionApprovalAudit } =
|
|
135
|
+
await import('./instruction-approval-audit.js');
|
|
95
136
|
|
|
96
137
|
const result = verifyInstructionBaseline();
|
|
97
138
|
if (result.ok) return;
|
|
@@ -107,7 +148,9 @@ async function ensureTuiInstructionApproval(commandName: string): Promise<void>
|
|
|
107
148
|
console.error(`Instruction baseline is invalid: ${result.baselineError}`);
|
|
108
149
|
console.error(`Baseline path: ${INSTRUCTION_BASELINE_PATH}`);
|
|
109
150
|
} else if (!result.baseline) {
|
|
110
|
-
console.error(
|
|
151
|
+
console.error(
|
|
152
|
+
`No approved instruction baseline found at ${INSTRUCTION_BASELINE_PATH}.`,
|
|
153
|
+
);
|
|
111
154
|
}
|
|
112
155
|
|
|
113
156
|
const changed = result.files.filter((file) => file.status !== 'ok');
|
|
@@ -133,10 +176,17 @@ async function ensureTuiInstructionApproval(commandName: string): Promise<void>
|
|
|
133
176
|
);
|
|
134
177
|
}
|
|
135
178
|
|
|
136
|
-
const rl = readline.createInterface({
|
|
179
|
+
const rl = readline.createInterface({
|
|
180
|
+
input: process.stdin,
|
|
181
|
+
output: process.stdout,
|
|
182
|
+
});
|
|
137
183
|
let answer = '';
|
|
138
184
|
try {
|
|
139
|
-
answer = (
|
|
185
|
+
answer = (
|
|
186
|
+
await rl.question('Approve current instruction changes now? [y/N] ')
|
|
187
|
+
)
|
|
188
|
+
.trim()
|
|
189
|
+
.toLowerCase();
|
|
140
190
|
} finally {
|
|
141
191
|
rl.close();
|
|
142
192
|
}
|
|
@@ -156,7 +206,9 @@ async function ensureTuiInstructionApproval(commandName: string): Promise<void>
|
|
|
156
206
|
|
|
157
207
|
try {
|
|
158
208
|
const baseline = approveInstructionBaseline();
|
|
159
|
-
console.log(
|
|
209
|
+
console.log(
|
|
210
|
+
`Approved instruction baseline at ${INSTRUCTION_BASELINE_PATH} (${baseline.approvedAt}).`,
|
|
211
|
+
);
|
|
160
212
|
completeInstructionApprovalAudit({
|
|
161
213
|
context: auditContext,
|
|
162
214
|
approved: true,
|
|
@@ -186,7 +238,10 @@ function printMainUsage(): void {
|
|
|
186
238
|
onboarding Run HybridAI account/API key onboarding
|
|
187
239
|
update Check and apply HybridClaw CLI updates
|
|
188
240
|
audit Inspect/verify structured audit trail
|
|
189
|
-
help Show general or topic-specific help (e.g. \`hybridclaw help gateway\`)
|
|
241
|
+
help Show general or topic-specific help (e.g. \`hybridclaw help gateway\`)
|
|
242
|
+
|
|
243
|
+
Options:
|
|
244
|
+
--version, -v Show HybridClaw CLI version`);
|
|
190
245
|
}
|
|
191
246
|
|
|
192
247
|
function printGatewayUsage(): void {
|
|
@@ -309,12 +364,19 @@ function readGatewayPid(): GatewayPidState | null {
|
|
|
309
364
|
try {
|
|
310
365
|
const raw = fs.readFileSync(GATEWAY_PID_PATH, 'utf-8');
|
|
311
366
|
const parsed = JSON.parse(raw) as Partial<GatewayPidState>;
|
|
312
|
-
if (
|
|
367
|
+
if (
|
|
368
|
+
!parsed ||
|
|
369
|
+
typeof parsed.pid !== 'number' ||
|
|
370
|
+
!Number.isFinite(parsed.pid)
|
|
371
|
+
)
|
|
372
|
+
return null;
|
|
313
373
|
return {
|
|
314
374
|
pid: parsed.pid,
|
|
315
375
|
startedAt: typeof parsed.startedAt === 'string' ? parsed.startedAt : '',
|
|
316
376
|
cwd: typeof parsed.cwd === 'string' ? parsed.cwd : '',
|
|
317
|
-
command: Array.isArray(parsed.command)
|
|
377
|
+
command: Array.isArray(parsed.command)
|
|
378
|
+
? parsed.command.map((item) => String(item))
|
|
379
|
+
: [],
|
|
318
380
|
};
|
|
319
381
|
} catch {
|
|
320
382
|
return null;
|
|
@@ -364,7 +426,11 @@ function parseGatewayBaseUrl(): URL | null {
|
|
|
364
426
|
|
|
365
427
|
function isLocalGatewayHost(hostname: string): boolean {
|
|
366
428
|
const normalized = hostname.trim().toLowerCase();
|
|
367
|
-
return
|
|
429
|
+
return (
|
|
430
|
+
normalized === '127.0.0.1' ||
|
|
431
|
+
normalized === 'localhost' ||
|
|
432
|
+
normalized === '::1'
|
|
433
|
+
);
|
|
368
434
|
}
|
|
369
435
|
|
|
370
436
|
function resolveGatewayListenPort(url: URL): number {
|
|
@@ -380,9 +446,13 @@ function findGatewayPidByPort(): number | null {
|
|
|
380
446
|
if (!parsed || !isLocalGatewayHost(parsed.hostname)) return null;
|
|
381
447
|
const port = resolveGatewayListenPort(parsed);
|
|
382
448
|
|
|
383
|
-
const result = spawnSync(
|
|
384
|
-
|
|
385
|
-
|
|
449
|
+
const result = spawnSync(
|
|
450
|
+
'lsof',
|
|
451
|
+
['-nP', `-iTCP:${port}`, '-sTCP:LISTEN', '-t'],
|
|
452
|
+
{
|
|
453
|
+
encoding: 'utf-8',
|
|
454
|
+
},
|
|
455
|
+
);
|
|
386
456
|
if (result.error) return null;
|
|
387
457
|
const output = (result.stdout || '').trim();
|
|
388
458
|
if (!output) return null;
|
|
@@ -408,12 +478,14 @@ function adoptGatewayPid(pid: number, source: string): boolean {
|
|
|
408
478
|
async function adoptReachableGatewayIfPossible(): Promise<boolean> {
|
|
409
479
|
const { gatewayStatus } = await import('./gateway-client.js');
|
|
410
480
|
const status = await gatewayStatus();
|
|
411
|
-
const pid =
|
|
412
|
-
|
|
413
|
-
|
|
481
|
+
const pid =
|
|
482
|
+
typeof status.pid === 'number' && Number.isFinite(status.pid)
|
|
483
|
+
? Math.floor(status.pid)
|
|
484
|
+
: 0;
|
|
414
485
|
if (pid > 0 && adoptGatewayPid(pid, 'api-status')) return true;
|
|
415
486
|
const fallbackPid = findGatewayPidByPort();
|
|
416
|
-
if (fallbackPid && adoptGatewayPid(fallbackPid, 'lsof-port-probe'))
|
|
487
|
+
if (fallbackPid && adoptGatewayPid(fallbackPid, 'lsof-port-probe'))
|
|
488
|
+
return true;
|
|
417
489
|
return false;
|
|
418
490
|
}
|
|
419
491
|
|
|
@@ -423,11 +495,16 @@ async function runGatewayForeground(commandName: string): Promise<void> {
|
|
|
423
495
|
await import('./gateway.js');
|
|
424
496
|
}
|
|
425
497
|
|
|
426
|
-
async function startGatewayBackend(
|
|
498
|
+
async function startGatewayBackend(
|
|
499
|
+
commandName: string,
|
|
500
|
+
waitForHealthy = false,
|
|
501
|
+
): Promise<void> {
|
|
427
502
|
if (await isGatewayReachable()) {
|
|
428
503
|
const existing = readGatewayPid();
|
|
429
504
|
if (existing && isPidRunning(existing.pid)) {
|
|
430
|
-
console.log(
|
|
505
|
+
console.log(
|
|
506
|
+
`Gateway already running in backend mode (pid ${existing.pid}).`,
|
|
507
|
+
);
|
|
431
508
|
} else {
|
|
432
509
|
let adopted = false;
|
|
433
510
|
try {
|
|
@@ -437,9 +514,13 @@ async function startGatewayBackend(commandName: string, waitForHealthy = false):
|
|
|
437
514
|
}
|
|
438
515
|
if (adopted) {
|
|
439
516
|
const adoptedState = readGatewayPid();
|
|
440
|
-
console.log(
|
|
517
|
+
console.log(
|
|
518
|
+
`Gateway already reachable at ${GATEWAY_BASE_URL}; adopted pid ${adoptedState?.pid || '(unknown)'}.`,
|
|
519
|
+
);
|
|
441
520
|
} else {
|
|
442
|
-
console.log(
|
|
521
|
+
console.log(
|
|
522
|
+
`Gateway already reachable at ${GATEWAY_BASE_URL} (unmanaged by CLI PID file).`,
|
|
523
|
+
);
|
|
443
524
|
}
|
|
444
525
|
}
|
|
445
526
|
return;
|
|
@@ -451,12 +532,14 @@ async function startGatewayBackend(commandName: string, waitForHealthy = false):
|
|
|
451
532
|
const healthy = await waitForGatewayReachable(15_000);
|
|
452
533
|
if (!healthy) {
|
|
453
534
|
throw new Error(
|
|
454
|
-
`Gateway process ${existing.pid} exists but did not become reachable at ${GATEWAY_BASE_URL}.`
|
|
455
|
-
|
|
535
|
+
`Gateway process ${existing.pid} exists but did not become reachable at ${GATEWAY_BASE_URL}.` +
|
|
536
|
+
` Check logs: ${GATEWAY_LOG_PATH}`,
|
|
456
537
|
);
|
|
457
538
|
}
|
|
458
539
|
}
|
|
459
|
-
console.log(
|
|
540
|
+
console.log(
|
|
541
|
+
`Gateway already running in backend mode (pid ${existing.pid}).`,
|
|
542
|
+
);
|
|
460
543
|
return;
|
|
461
544
|
}
|
|
462
545
|
if (existing && !isPidRunning(existing.pid)) {
|
|
@@ -494,8 +577,8 @@ async function startGatewayBackend(commandName: string, waitForHealthy = false):
|
|
|
494
577
|
const healthy = await waitForGatewayReachable(20_000);
|
|
495
578
|
if (!healthy) {
|
|
496
579
|
throw new Error(
|
|
497
|
-
`Gateway backend started (pid ${child.pid}) but not reachable at ${GATEWAY_BASE_URL}.`
|
|
498
|
-
|
|
580
|
+
`Gateway backend started (pid ${child.pid}) but not reachable at ${GATEWAY_BASE_URL}.` +
|
|
581
|
+
` Check logs: ${GATEWAY_LOG_PATH}`,
|
|
499
582
|
);
|
|
500
583
|
}
|
|
501
584
|
}
|
|
@@ -514,7 +597,9 @@ async function stopManagedGatewayByPid(state: GatewayPidState): Promise<void> {
|
|
|
514
597
|
process.kill(state.pid, 'SIGTERM');
|
|
515
598
|
} catch (err) {
|
|
516
599
|
removeGatewayPidFile();
|
|
517
|
-
throw new Error(
|
|
600
|
+
throw new Error(
|
|
601
|
+
`Failed to stop gateway pid ${state.pid}: ${err instanceof Error ? err.message : String(err)}`,
|
|
602
|
+
);
|
|
518
603
|
}
|
|
519
604
|
|
|
520
605
|
const deadline = Date.now() + 10_000;
|
|
@@ -538,7 +623,9 @@ async function stopManagedGatewayByPid(state: GatewayPidState): Promise<void> {
|
|
|
538
623
|
console.log(`Gateway stop timed out; sent SIGKILL to pid ${state.pid}.`);
|
|
539
624
|
}
|
|
540
625
|
|
|
541
|
-
async function stopUnmanagedGatewayGracefully(
|
|
626
|
+
async function stopUnmanagedGatewayGracefully(
|
|
627
|
+
mode: 'stop' | 'restart',
|
|
628
|
+
): Promise<void> {
|
|
542
629
|
const suffix = mode === 'restart' ? ' before restart' : '';
|
|
543
630
|
console.log('Gateway is reachable but unmanaged by CLI PID file.');
|
|
544
631
|
console.log(`Requesting graceful shutdown over API${suffix}...`);
|
|
@@ -548,8 +635,8 @@ async function stopUnmanagedGatewayGracefully(mode: 'stop' | 'restart'): Promise
|
|
|
548
635
|
const stopped = await waitForGatewayUnreachable(10_000);
|
|
549
636
|
if (!stopped) {
|
|
550
637
|
throw new Error(
|
|
551
|
-
`Gateway remained reachable at ${GATEWAY_BASE_URL} after shutdown request.`
|
|
552
|
-
|
|
638
|
+
`Gateway remained reachable at ${GATEWAY_BASE_URL} after shutdown request.` +
|
|
639
|
+
' Stop it from its owning process or use your system process manager.',
|
|
553
640
|
);
|
|
554
641
|
}
|
|
555
642
|
console.log('Unmanaged gateway stopped via API request.');
|
|
@@ -559,18 +646,24 @@ async function stopUnmanagedGatewayGracefully(mode: 'stop' | 'restart'): Promise
|
|
|
559
646
|
}
|
|
560
647
|
|
|
561
648
|
const discoveredPid = findGatewayPidByPort();
|
|
562
|
-
if (
|
|
649
|
+
if (
|
|
650
|
+
discoveredPid &&
|
|
651
|
+
discoveredPid !== process.pid &&
|
|
652
|
+
adoptGatewayPid(discoveredPid, 'lsof-port-probe')
|
|
653
|
+
) {
|
|
563
654
|
const adoptedState = readGatewayPid();
|
|
564
655
|
if (adoptedState && isPidRunning(adoptedState.pid)) {
|
|
565
|
-
console.log(
|
|
656
|
+
console.log(
|
|
657
|
+
`Shutdown API unavailable; stopping gateway pid ${adoptedState.pid} via local signal.`,
|
|
658
|
+
);
|
|
566
659
|
await stopManagedGatewayByPid(adoptedState);
|
|
567
660
|
return;
|
|
568
661
|
}
|
|
569
662
|
}
|
|
570
663
|
|
|
571
664
|
throw new Error(
|
|
572
|
-
`Gateway shutdown endpoint is unavailable at ${GATEWAY_BASE_URL} and PID ownership could not be recovered.`
|
|
573
|
-
|
|
665
|
+
`Gateway shutdown endpoint is unavailable at ${GATEWAY_BASE_URL} and PID ownership could not be recovered.` +
|
|
666
|
+
' Stop the process manually once, then retry.',
|
|
574
667
|
);
|
|
575
668
|
}
|
|
576
669
|
|
|
@@ -588,11 +681,15 @@ async function stopGatewayBackend(): Promise<void> {
|
|
|
588
681
|
if (!isPidRunning(state.pid)) {
|
|
589
682
|
removeGatewayPidFile();
|
|
590
683
|
if (await isGatewayReachable()) {
|
|
591
|
-
console.log(
|
|
684
|
+
console.log(
|
|
685
|
+
`Removed stale gateway PID file (pid ${state.pid} not running).`,
|
|
686
|
+
);
|
|
592
687
|
await stopUnmanagedGatewayGracefully('stop');
|
|
593
688
|
return;
|
|
594
689
|
}
|
|
595
|
-
console.log(
|
|
690
|
+
console.log(
|
|
691
|
+
`Removed stale gateway PID file (pid ${state.pid} not running).`,
|
|
692
|
+
);
|
|
596
693
|
return;
|
|
597
694
|
}
|
|
598
695
|
|
|
@@ -611,21 +708,29 @@ async function printGatewayLifecycleStatus(): Promise<void> {
|
|
|
611
708
|
} else {
|
|
612
709
|
console.log('PID file: not found');
|
|
613
710
|
}
|
|
614
|
-
console.log(
|
|
711
|
+
console.log(
|
|
712
|
+
`Gateway API reachable: ${reachable ? 'yes' : 'no'} (${GATEWAY_BASE_URL})`,
|
|
713
|
+
);
|
|
615
714
|
|
|
616
715
|
if (reachable) {
|
|
617
716
|
try {
|
|
618
717
|
const { gatewayStatus } = await import('./gateway-client.js');
|
|
619
718
|
const status = await gatewayStatus();
|
|
620
|
-
console.log(
|
|
719
|
+
console.log(
|
|
720
|
+
`Uptime: ${status.uptime}s | Sessions: ${status.sessions} | Containers: ${status.activeContainers}`,
|
|
721
|
+
);
|
|
621
722
|
} catch (err) {
|
|
622
|
-
console.log(
|
|
723
|
+
console.log(
|
|
724
|
+
`Gateway status fetch failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
725
|
+
);
|
|
623
726
|
}
|
|
624
727
|
}
|
|
625
728
|
}
|
|
626
729
|
|
|
627
730
|
async function runGatewayApiCommand(args: string[]): Promise<void> {
|
|
628
|
-
const { gatewayCommand, renderGatewayCommand } = await import(
|
|
731
|
+
const { gatewayCommand, renderGatewayCommand } = await import(
|
|
732
|
+
'./gateway-client.js'
|
|
733
|
+
);
|
|
629
734
|
const result = await gatewayCommand({
|
|
630
735
|
sessionId: 'cli:gateway',
|
|
631
736
|
guildId: null,
|
|
@@ -703,6 +808,10 @@ async function main(): Promise<void> {
|
|
|
703
808
|
const subargs = process.argv.slice(3);
|
|
704
809
|
|
|
705
810
|
switch (command) {
|
|
811
|
+
case '--version':
|
|
812
|
+
case '-v':
|
|
813
|
+
console.log(APP_VERSION);
|
|
814
|
+
break;
|
|
706
815
|
case 'gateway':
|
|
707
816
|
await handleGatewayCommand(subargs);
|
|
708
817
|
break;
|
|
@@ -722,7 +831,10 @@ async function main(): Promise<void> {
|
|
|
722
831
|
printOnboardingUsage();
|
|
723
832
|
break;
|
|
724
833
|
}
|
|
725
|
-
await ensureHybridAICredentials({
|
|
834
|
+
await ensureHybridAICredentials({
|
|
835
|
+
force: true,
|
|
836
|
+
commandName: 'hybridclaw onboarding',
|
|
837
|
+
});
|
|
726
838
|
await ensureRuntimeContainer('hybridclaw onboarding', false);
|
|
727
839
|
break;
|
|
728
840
|
case 'update': {
|
|
@@ -772,16 +884,21 @@ const envVarHint: Record<string, string> = {
|
|
|
772
884
|
};
|
|
773
885
|
|
|
774
886
|
function printMissingEnvVarError(message: string, envVar?: string): void {
|
|
775
|
-
const hint = envVar
|
|
887
|
+
const hint = envVar
|
|
888
|
+
? envVarHint[envVar]
|
|
889
|
+
: 'Set this variable and rerun the command.';
|
|
776
890
|
console.error(`hybridclaw error: ${message}`);
|
|
777
891
|
console.error(`Hint: ${hint}`);
|
|
778
|
-
console.error(
|
|
892
|
+
console.error(
|
|
893
|
+
'Make sure you run `hybridclaw` from the directory that contains your .env file.',
|
|
894
|
+
);
|
|
779
895
|
}
|
|
780
896
|
|
|
781
897
|
main().catch((err) => {
|
|
782
|
-
const missingEnvVarMatch =
|
|
783
|
-
|
|
784
|
-
|
|
898
|
+
const missingEnvVarMatch =
|
|
899
|
+
err instanceof Error
|
|
900
|
+
? err.message.match(/^Missing required env var:\s*([A-Za-z0-9_]+)/)
|
|
901
|
+
: null;
|
|
785
902
|
if (missingEnvVarMatch) {
|
|
786
903
|
printMissingEnvVarError(err.message, missingEnvVarMatch[1]);
|
|
787
904
|
} else if (err instanceof MissingRequiredEnvVarError) {
|