clawmoney 0.17.41 → 0.17.43
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/dist/commands/task.d.ts +3 -0
- package/dist/commands/task.js +94 -0
- package/dist/index.js +43 -0
- package/dist/task/daemon.d.ts +2 -1
- package/dist/task/daemon.js +70 -5
- package/package.json +1 -1
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { openSync, closeSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import ora from "ora";
|
|
7
|
+
import { requireConfig } from "../utils/config.js";
|
|
8
|
+
import { readTaskPid, isPidAlive } from "../task/daemon.js";
|
|
9
|
+
const LOG_FILE = join(homedir(), ".clawmoney", "task.log");
|
|
10
|
+
export async function taskStartCommand() {
|
|
11
|
+
const config = requireConfig();
|
|
12
|
+
const existing = readTaskPid();
|
|
13
|
+
if (existing !== null && isPidAlive(existing)) {
|
|
14
|
+
console.log(chalk.yellow(`Task daemon is already running (PID ${existing}). Use "clawmoney task stop" first.`));
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const spinner = ora("Starting Task daemon...").start();
|
|
18
|
+
try {
|
|
19
|
+
const thisDir = import.meta.url.replace("file://", "").replace(/\/[^/]+$/, "");
|
|
20
|
+
const parentDir = thisDir.replace(/\/[^/]+$/, "");
|
|
21
|
+
const daemonScript = join(parentDir, "task", "daemon.js");
|
|
22
|
+
// Open log file for stdout/stderr capture (daemon detaches from parent).
|
|
23
|
+
const out = openSync(LOG_FILE, "a");
|
|
24
|
+
const err = openSync(LOG_FILE, "a");
|
|
25
|
+
const child = spawn(process.execPath, [daemonScript], {
|
|
26
|
+
stdio: ["ignore", out, err],
|
|
27
|
+
detached: true,
|
|
28
|
+
env: {
|
|
29
|
+
...process.env,
|
|
30
|
+
CLAWMONEY_DAEMON: "1",
|
|
31
|
+
// daemon reads these from env first, then ~/.clawmoney/config.yaml as fallback
|
|
32
|
+
API_KEY: config.api_key,
|
|
33
|
+
AGENT_ID: config.agent_id ?? "",
|
|
34
|
+
AGENT_NAME: config.agent_slug ?? "clawmoney-task",
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
child.unref();
|
|
38
|
+
// Release fds in the parent — keeping them open keeps node's event
|
|
39
|
+
// loop alive (the parent never exits), and stale parent processes
|
|
40
|
+
// mean each desktop dashboard refresh would re-spawn another daemon.
|
|
41
|
+
closeSync(out);
|
|
42
|
+
closeSync(err);
|
|
43
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
44
|
+
const pid = readTaskPid();
|
|
45
|
+
if (pid && isPidAlive(pid)) {
|
|
46
|
+
spinner.succeed(chalk.green(`Task daemon started (PID ${pid})`));
|
|
47
|
+
console.log(chalk.dim(` Log file: ${LOG_FILE}`));
|
|
48
|
+
console.log(chalk.dim(` Hub: wss://api.spareapi.ai/ws/relay`));
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
spinner.fail(chalk.red(`Failed to start Task daemon. Check logs at: ${LOG_FILE}`));
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
spinner.fail(chalk.red("Failed to start Task daemon"));
|
|
57
|
+
throw e;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export async function taskStopCommand() {
|
|
61
|
+
const pid = readTaskPid();
|
|
62
|
+
if (pid === null) {
|
|
63
|
+
console.log(chalk.dim("Task daemon is not running (no PID file)."));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (!isPidAlive(pid)) {
|
|
67
|
+
console.log(chalk.dim(`Task daemon PID ${pid} not alive. Cleaning up.`));
|
|
68
|
+
// daemon's own SIGTERM handler removes the PID file; if dead, we'd
|
|
69
|
+
// typically clean here. Skip for simplicity — next start replaces.
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
process.kill(pid, "SIGTERM");
|
|
74
|
+
console.log(chalk.green(`Task daemon stopped (PID ${pid}).`));
|
|
75
|
+
}
|
|
76
|
+
catch (e) {
|
|
77
|
+
console.error(chalk.red(`Failed to stop process ${pid}:`), e.message);
|
|
78
|
+
}
|
|
79
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
80
|
+
}
|
|
81
|
+
export async function taskStatusCommand() {
|
|
82
|
+
const pid = readTaskPid();
|
|
83
|
+
if (pid === null) {
|
|
84
|
+
console.log(chalk.dim("Task daemon is not running."));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (isPidAlive(pid)) {
|
|
88
|
+
console.log(chalk.green(`Task daemon is running (PID ${pid}).`));
|
|
89
|
+
console.log(chalk.dim(` Log file: ${LOG_FILE}`));
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
console.log(chalk.yellow(`Task daemon PID ${pid} not alive (stale PID file).`));
|
|
93
|
+
}
|
|
94
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -367,6 +367,49 @@ market
|
|
|
367
367
|
process.exit(1);
|
|
368
368
|
}
|
|
369
369
|
});
|
|
370
|
+
// task (spareai-hub OpenCLI sync gateway)
|
|
371
|
+
const task = program
|
|
372
|
+
.command('task')
|
|
373
|
+
.description('Task daemon: serve sync OpenCLI requests via spareai-hub');
|
|
374
|
+
task
|
|
375
|
+
.command('start')
|
|
376
|
+
.description('Start Task daemon (background process)')
|
|
377
|
+
.action(async () => {
|
|
378
|
+
try {
|
|
379
|
+
const { taskStartCommand } = await import('./commands/task.js');
|
|
380
|
+
await taskStartCommand();
|
|
381
|
+
}
|
|
382
|
+
catch (err) {
|
|
383
|
+
console.error(err.message);
|
|
384
|
+
process.exit(1);
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
task
|
|
388
|
+
.command('stop')
|
|
389
|
+
.description('Stop Task daemon')
|
|
390
|
+
.action(async () => {
|
|
391
|
+
try {
|
|
392
|
+
const { taskStopCommand } = await import('./commands/task.js');
|
|
393
|
+
await taskStopCommand();
|
|
394
|
+
}
|
|
395
|
+
catch (err) {
|
|
396
|
+
console.error(err.message);
|
|
397
|
+
process.exit(1);
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
task
|
|
401
|
+
.command('status')
|
|
402
|
+
.description('Check Task daemon status')
|
|
403
|
+
.action(async () => {
|
|
404
|
+
try {
|
|
405
|
+
const { taskStatusCommand } = await import('./commands/task.js');
|
|
406
|
+
await taskStatusCommand();
|
|
407
|
+
}
|
|
408
|
+
catch (err) {
|
|
409
|
+
console.error(err.message);
|
|
410
|
+
process.exit(1);
|
|
411
|
+
}
|
|
412
|
+
});
|
|
370
413
|
// gig (escrow tasks)
|
|
371
414
|
const gig = program.command('gig').description('Gig marketplace: post and accept freelance tasks');
|
|
372
415
|
gig
|
package/dist/task/daemon.d.ts
CHANGED
package/dist/task/daemon.js
CHANGED
|
@@ -20,9 +20,34 @@
|
|
|
20
20
|
* No CLI integration yet — that lands in Phase 3 alongside the proper
|
|
21
21
|
* `clawmoney task start` command.
|
|
22
22
|
*/
|
|
23
|
+
import { readFileSync, writeFileSync, unlinkSync, existsSync } from "node:fs";
|
|
24
|
+
import { join } from "node:path";
|
|
25
|
+
import { homedir } from "node:os";
|
|
26
|
+
import YAML from "yaml";
|
|
23
27
|
import { TaskWsClient } from "./ws-client.js";
|
|
24
28
|
import { getSkill, listSkills } from "./skills/index.js";
|
|
25
|
-
|
|
29
|
+
const CONFIG_DIR = join(homedir(), ".clawmoney");
|
|
30
|
+
const CONFIG_FILE = join(CONFIG_DIR, "config.yaml");
|
|
31
|
+
const PID_FILE = join(CONFIG_DIR, "task.pid");
|
|
32
|
+
function loadYamlConfig() {
|
|
33
|
+
if (!existsSync(CONFIG_FILE))
|
|
34
|
+
return {};
|
|
35
|
+
try {
|
|
36
|
+
return (YAML.parse(readFileSync(CONFIG_FILE, "utf-8")) ?? {});
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function pickString(...candidates) {
|
|
43
|
+
for (const c of candidates) {
|
|
44
|
+
if (typeof c === "string" && c)
|
|
45
|
+
return c;
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
function loadConfig() {
|
|
50
|
+
const yaml = loadYamlConfig();
|
|
26
51
|
const skillsEnv = process.env.SKILLS ?? "";
|
|
27
52
|
const requested = skillsEnv
|
|
28
53
|
.split(",")
|
|
@@ -41,9 +66,9 @@ function loadConfigFromEnv() {
|
|
|
41
66
|
});
|
|
42
67
|
return {
|
|
43
68
|
hub_url: process.env.HUB_URL ?? "wss://api.spareapi.ai/ws/relay",
|
|
44
|
-
api_key: process.env.API_KEY ?? "",
|
|
45
|
-
agent_id: process.env.AGENT_ID,
|
|
46
|
-
agent_name: process.env.AGENT_NAME ?? "clawmoney-task",
|
|
69
|
+
api_key: pickString(process.env.API_KEY, yaml.api_key) ?? "",
|
|
70
|
+
agent_id: pickString(process.env.AGENT_ID, yaml.agent_id),
|
|
71
|
+
agent_name: pickString(process.env.AGENT_NAME, yaml.agent_slug) ?? "clawmoney-task",
|
|
47
72
|
skills: filtered,
|
|
48
73
|
max_concurrency: process.env.MAX_CONCURRENCY
|
|
49
74
|
? Number.parseInt(process.env.MAX_CONCURRENCY, 10)
|
|
@@ -52,6 +77,35 @@ function loadConfigFromEnv() {
|
|
|
52
77
|
reconnect: { initial_ms: 1000, max_ms: 60_000, multiplier: 2 },
|
|
53
78
|
};
|
|
54
79
|
}
|
|
80
|
+
export function readTaskPid() {
|
|
81
|
+
try {
|
|
82
|
+
const pid = parseInt(readFileSync(PID_FILE, "utf-8").trim(), 10);
|
|
83
|
+
return Number.isFinite(pid) ? pid : null;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
export function isPidAlive(pid) {
|
|
90
|
+
try {
|
|
91
|
+
process.kill(pid, 0);
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function writePid() {
|
|
99
|
+
writeFileSync(PID_FILE, String(process.pid), "utf-8");
|
|
100
|
+
}
|
|
101
|
+
function removePid() {
|
|
102
|
+
try {
|
|
103
|
+
unlinkSync(PID_FILE);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// ignore
|
|
107
|
+
}
|
|
108
|
+
}
|
|
55
109
|
async function handleTaskRequest(ws, req) {
|
|
56
110
|
const startedAtMs = Date.now();
|
|
57
111
|
const handler = getSkill(req.skill_id);
|
|
@@ -111,12 +165,22 @@ async function handleTaskRequest(ws, req) {
|
|
|
111
165
|
}
|
|
112
166
|
}
|
|
113
167
|
function main() {
|
|
114
|
-
const
|
|
168
|
+
const existing = readTaskPid();
|
|
169
|
+
if (existing !== null && isPidAlive(existing)) {
|
|
170
|
+
console.error(`[task] already running (PID ${existing})`);
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
const config = loadConfig();
|
|
174
|
+
if (!config.api_key) {
|
|
175
|
+
console.error("[task] api_key missing — set API_KEY env or run `clawmoney setup`");
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
115
178
|
console.log(`[task] starting daemon hub=${config.hub_url} skills=[${config.skills.join(",")}]`);
|
|
116
179
|
if (config.skills.length === 0) {
|
|
117
180
|
console.error("[task] no skills to advertise — refusing to start");
|
|
118
181
|
process.exit(1);
|
|
119
182
|
}
|
|
183
|
+
writePid();
|
|
120
184
|
// Belt-and-suspenders: even if every other handle (WS, heartbeat,
|
|
121
185
|
// reconnect timer) somehow goes away simultaneously, this interval
|
|
122
186
|
// is unref-less and ref-counted into the event loop, so the daemon
|
|
@@ -151,6 +215,7 @@ function main() {
|
|
|
151
215
|
const shutdown = (signal) => {
|
|
152
216
|
console.log(`[task] ${signal} — shutting down`);
|
|
153
217
|
ws.stop();
|
|
218
|
+
removePid();
|
|
154
219
|
setTimeout(() => process.exit(0), 200);
|
|
155
220
|
};
|
|
156
221
|
process.on("SIGINT", () => shutdown("SIGINT"));
|