@openclaw-cloud/agent-controller 0.2.1 → 0.2.3
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/__tests__/heartbeat.test.ts +53 -2
- package/dist/commands/install.js +13 -4
- package/dist/commands/install.js.map +1 -1
- package/dist/heartbeat.d.ts +1 -0
- package/dist/heartbeat.js +19 -0
- package/dist/heartbeat.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
- package/src/commands/install.ts +18 -4
- package/src/heartbeat.ts +22 -0
- package/src/types.ts +1 -0
|
@@ -1,8 +1,54 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
2
|
|
|
3
|
-
import { getMetrics, startHeartbeat } from '../src/heartbeat';
|
|
3
|
+
import { getMetrics, getLastMessageAt, startHeartbeat } from '../src/heartbeat';
|
|
4
4
|
import type { AgentApi } from '../src/api';
|
|
5
5
|
|
|
6
|
+
jest.mock('node:fs/promises', () => ({
|
|
7
|
+
readdir: jest.fn(),
|
|
8
|
+
stat: jest.fn(),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
import fsMock from 'node:fs/promises';
|
|
12
|
+
const fsPromises = fsMock as jest.Mocked<typeof fsMock>;
|
|
13
|
+
|
|
14
|
+
describe('getLastMessageAt', () => {
|
|
15
|
+
const origEnv = process.env.OPENCLAW_STATE_DIR;
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
process.env.OPENCLAW_STATE_DIR = origEnv;
|
|
19
|
+
jest.clearAllMocks();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('returns null when sessions directory does not exist', async () => {
|
|
23
|
+
fsPromises.readdir.mockRejectedValue(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }));
|
|
24
|
+
const result = await getLastMessageAt();
|
|
25
|
+
expect(result).toBeNull();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('returns null when no .jsonl files are present', async () => {
|
|
29
|
+
fsPromises.readdir.mockResolvedValue(['session.txt', 'other.json'] as unknown as Awaited<ReturnType<typeof fsPromises.readdir>>);
|
|
30
|
+
const result = await getLastMessageAt();
|
|
31
|
+
expect(result).toBeNull();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('returns ISO string of the most recent mtime when .jsonl files are present', async () => {
|
|
35
|
+
process.env.OPENCLAW_STATE_DIR = '/test/state';
|
|
36
|
+
fsPromises.readdir.mockResolvedValue(['a.jsonl', 'b.jsonl'] as unknown as Awaited<ReturnType<typeof fsPromises.readdir>>);
|
|
37
|
+
const t1 = new Date('2024-01-01T10:00:00.000Z').getTime();
|
|
38
|
+
const t2 = new Date('2024-01-02T12:00:00.000Z').getTime();
|
|
39
|
+
fsPromises.stat
|
|
40
|
+
.mockResolvedValueOnce({ mtime: new Date(t1) } as unknown as Awaited<ReturnType<typeof fsPromises.stat>>)
|
|
41
|
+
.mockResolvedValueOnce({ mtime: new Date(t2) } as unknown as Awaited<ReturnType<typeof fsPromises.stat>>);
|
|
42
|
+
const result = await getLastMessageAt();
|
|
43
|
+
expect(result).toBe('2024-01-02T12:00:00.000Z');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('does not throw on any error', async () => {
|
|
47
|
+
fsPromises.readdir.mockRejectedValue(new Error('unexpected'));
|
|
48
|
+
await expect(getLastMessageAt()).resolves.toBeNull();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
6
52
|
describe('getMetrics', () => {
|
|
7
53
|
it('returns cpu, mem, uptime', () => {
|
|
8
54
|
const metrics = getMetrics();
|
|
@@ -37,11 +83,14 @@ describe('startHeartbeat', () => {
|
|
|
37
83
|
jest.useRealTimers();
|
|
38
84
|
});
|
|
39
85
|
|
|
40
|
-
it('publishes heartbeat immediately and at intervals', () => {
|
|
86
|
+
it('publishes heartbeat immediately and at intervals', async () => {
|
|
41
87
|
const publishHeartbeat = jest.fn().mockResolvedValue(undefined);
|
|
42
88
|
const api = makeApi(publishHeartbeat);
|
|
43
89
|
|
|
44
90
|
const timer = startHeartbeat({ api, agentId: 'agent-42' });
|
|
91
|
+
// Flush microtasks so async getLastMessageAt() resolves before api.publishHeartbeat is called
|
|
92
|
+
await Promise.resolve();
|
|
93
|
+
await Promise.resolve();
|
|
45
94
|
|
|
46
95
|
// Immediate call
|
|
47
96
|
expect(publishHeartbeat).toHaveBeenCalledTimes(1);
|
|
@@ -55,6 +104,8 @@ describe('startHeartbeat', () => {
|
|
|
55
104
|
|
|
56
105
|
// After 30s
|
|
57
106
|
jest.advanceTimersByTime(30_000);
|
|
107
|
+
await Promise.resolve();
|
|
108
|
+
await Promise.resolve();
|
|
58
109
|
expect(publishHeartbeat).toHaveBeenCalledTimes(2);
|
|
59
110
|
|
|
60
111
|
clearInterval(timer);
|
package/dist/commands/install.js
CHANGED
|
@@ -6,15 +6,24 @@ async function prompt(rl, question) {
|
|
|
6
6
|
});
|
|
7
7
|
});
|
|
8
8
|
}
|
|
9
|
+
async function getOrPrompt(rl, envKey, question) {
|
|
10
|
+
const fromEnv = process.env[envKey];
|
|
11
|
+
if (fromEnv) {
|
|
12
|
+
const display = envKey.includes('TOKEN') ? '***' : fromEnv;
|
|
13
|
+
console.log(` ${envKey}: ${display} (from environment)`);
|
|
14
|
+
return fromEnv;
|
|
15
|
+
}
|
|
16
|
+
return prompt(rl, question);
|
|
17
|
+
}
|
|
9
18
|
export async function install() {
|
|
10
19
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
11
20
|
console.log('OpenClaw Agent Controller — Installation Wizard');
|
|
12
21
|
console.log('================================================');
|
|
13
22
|
console.log('');
|
|
14
|
-
const centrifugoUrl = await
|
|
15
|
-
const agentToken = await
|
|
16
|
-
const agentId = await
|
|
17
|
-
const backendInternalUrl = await
|
|
23
|
+
const centrifugoUrl = await getOrPrompt(rl, 'CENTRIFUGO_URL', 'CENTRIFUGO_URL (e.g. wss://ws.openclaw-cloud.io): ');
|
|
24
|
+
const agentToken = await getOrPrompt(rl, 'AGENT_TOKEN', 'AGENT_TOKEN: ');
|
|
25
|
+
const agentId = await getOrPrompt(rl, 'AGENT_ID', 'AGENT_ID: ');
|
|
26
|
+
const backendInternalUrl = await getOrPrompt(rl, 'BACKEND_INTERNAL_URL', 'BACKEND_INTERNAL_URL (e.g. https://api.openclaw-cloud.io): ');
|
|
18
27
|
rl.close();
|
|
19
28
|
if (!centrifugoUrl || !agentToken || !agentId || !backendInternalUrl) {
|
|
20
29
|
console.error('All fields are required. Aborting.');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"install.js","sourceRoot":"","sources":["../../src/commands/install.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAEhD,KAAK,UAAU,MAAM,CAAC,EAAsC,EAAE,QAAgB;IAC5E,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,EAAE,CAAC,QAAQ,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,EAAE;YAC/B,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;QACzB,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,OAAO;IAC3B,MAAM,EAAE,GAAG,eAAe,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAE7E,OAAO,CAAC,GAAG,CAAC,iDAAiD,CAAC,CAAC;IAC/D,OAAO,CAAC,GAAG,CAAC,kDAAkD,CAAC,CAAC;IAChE,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAEhB,MAAM,aAAa,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"install.js","sourceRoot":"","sources":["../../src/commands/install.ts"],"names":[],"mappings":"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"}
|
package/dist/heartbeat.d.ts
CHANGED
package/dist/heartbeat.js
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
2
4
|
import { debugLog } from './debug.js';
|
|
3
5
|
const HEARTBEAT_INTERVAL = 30_000;
|
|
6
|
+
export async function getLastMessageAt() {
|
|
7
|
+
const stateDir = process.env.OPENCLAW_STATE_DIR?.trim()
|
|
8
|
+
?? path.join(os.homedir(), '.openclaw');
|
|
9
|
+
const sessionsDir = path.join(stateDir, 'agents', 'main', 'sessions');
|
|
10
|
+
try {
|
|
11
|
+
const files = await fs.readdir(sessionsDir);
|
|
12
|
+
const jsonlFiles = files.filter((f) => f.endsWith('.jsonl'));
|
|
13
|
+
if (jsonlFiles.length === 0)
|
|
14
|
+
return null;
|
|
15
|
+
const mtimes = await Promise.all(jsonlFiles.map((f) => fs.stat(path.join(sessionsDir, f)).then((s) => s.mtime.getTime())));
|
|
16
|
+
return new Date(Math.max(...mtimes)).toISOString();
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
4
22
|
function getMetrics() {
|
|
5
23
|
const cpus = os.cpus();
|
|
6
24
|
const cpuUsage = cpus.reduce((acc, cpu) => {
|
|
@@ -22,6 +40,7 @@ async function publishHeartbeat(opts) {
|
|
|
22
40
|
ts: Date.now(),
|
|
23
41
|
metrics: getMetrics(),
|
|
24
42
|
...(opts.getBoardStatus ? { boardStatus: opts.getBoardStatus() } : {}),
|
|
43
|
+
lastMessageAt: await getLastMessageAt(),
|
|
25
44
|
};
|
|
26
45
|
try {
|
|
27
46
|
debugLog('heartbeat', '→ sending', opts.agentId, JSON.stringify(payload).slice(0, 500));
|
package/dist/heartbeat.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"heartbeat.js","sourceRoot":"","sources":["../src/heartbeat.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAItC,MAAM,kBAAkB,GAAG,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"heartbeat.js","sourceRoot":"","sources":["../src/heartbeat.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
CHANGED
package/package.json
CHANGED
package/src/commands/install.ts
CHANGED
|
@@ -8,6 +8,20 @@ async function prompt(rl: ReturnType<typeof createInterface>, question: string):
|
|
|
8
8
|
});
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
async function getOrPrompt(
|
|
12
|
+
rl: ReturnType<typeof createInterface>,
|
|
13
|
+
envKey: string,
|
|
14
|
+
question: string,
|
|
15
|
+
): Promise<string> {
|
|
16
|
+
const fromEnv = process.env[envKey];
|
|
17
|
+
if (fromEnv) {
|
|
18
|
+
const display = envKey.includes('TOKEN') ? '***' : fromEnv;
|
|
19
|
+
console.log(` ${envKey}: ${display} (from environment)`);
|
|
20
|
+
return fromEnv;
|
|
21
|
+
}
|
|
22
|
+
return prompt(rl, question);
|
|
23
|
+
}
|
|
24
|
+
|
|
11
25
|
export async function install(): Promise<void> {
|
|
12
26
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
13
27
|
|
|
@@ -15,10 +29,10 @@ export async function install(): Promise<void> {
|
|
|
15
29
|
console.log('================================================');
|
|
16
30
|
console.log('');
|
|
17
31
|
|
|
18
|
-
const centrifugoUrl = await
|
|
19
|
-
const agentToken = await
|
|
20
|
-
const agentId = await
|
|
21
|
-
const backendInternalUrl = await
|
|
32
|
+
const centrifugoUrl = await getOrPrompt(rl, 'CENTRIFUGO_URL', 'CENTRIFUGO_URL (e.g. wss://ws.openclaw-cloud.io): ');
|
|
33
|
+
const agentToken = await getOrPrompt(rl, 'AGENT_TOKEN', 'AGENT_TOKEN: ');
|
|
34
|
+
const agentId = await getOrPrompt(rl, 'AGENT_ID', 'AGENT_ID: ');
|
|
35
|
+
const backendInternalUrl = await getOrPrompt(rl, 'BACKEND_INTERNAL_URL', 'BACKEND_INTERNAL_URL (e.g. https://api.openclaw-cloud.io): ');
|
|
22
36
|
|
|
23
37
|
rl.close();
|
|
24
38
|
|
package/src/heartbeat.ts
CHANGED
|
@@ -1,10 +1,31 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
2
4
|
import { debugLog } from './debug.js';
|
|
3
5
|
import type { AgentApi } from './api.js';
|
|
4
6
|
import type { BoardState, HeartbeatPayload } from './types.js';
|
|
5
7
|
|
|
6
8
|
const HEARTBEAT_INTERVAL = 30_000;
|
|
7
9
|
|
|
10
|
+
export async function getLastMessageAt(): Promise<string | null> {
|
|
11
|
+
const stateDir = process.env.OPENCLAW_STATE_DIR?.trim()
|
|
12
|
+
?? path.join(os.homedir(), '.openclaw');
|
|
13
|
+
const sessionsDir = path.join(stateDir, 'agents', 'main', 'sessions');
|
|
14
|
+
try {
|
|
15
|
+
const files = await fs.readdir(sessionsDir);
|
|
16
|
+
const jsonlFiles = files.filter((f) => f.endsWith('.jsonl'));
|
|
17
|
+
if (jsonlFiles.length === 0) return null;
|
|
18
|
+
const mtimes = await Promise.all(
|
|
19
|
+
jsonlFiles.map((f) =>
|
|
20
|
+
fs.stat(path.join(sessionsDir, f)).then((s) => s.mtime.getTime())
|
|
21
|
+
)
|
|
22
|
+
);
|
|
23
|
+
return new Date(Math.max(...mtimes)).toISOString();
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
8
29
|
export interface HeartbeatOptions {
|
|
9
30
|
api: AgentApi;
|
|
10
31
|
agentId: string;
|
|
@@ -35,6 +56,7 @@ async function publishHeartbeat(opts: HeartbeatOptions): Promise<void> {
|
|
|
35
56
|
ts: Date.now(),
|
|
36
57
|
metrics: getMetrics(),
|
|
37
58
|
...(opts.getBoardStatus ? { boardStatus: opts.getBoardStatus() } : {}),
|
|
59
|
+
lastMessageAt: await getLastMessageAt(),
|
|
38
60
|
};
|
|
39
61
|
|
|
40
62
|
try {
|