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.
@@ -0,0 +1,3 @@
1
+ export declare function taskStartCommand(): Promise<void>;
2
+ export declare function taskStopCommand(): Promise<void>;
3
+ export declare function taskStatusCommand(): Promise<void>;
@@ -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
@@ -1,2 +1,3 @@
1
1
  #!/usr/bin/env tsx
2
- export {};
2
+ export declare function readTaskPid(): number | null;
3
+ export declare function isPidAlive(pid: number): boolean;
@@ -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
- function loadConfigFromEnv() {
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 config = loadConfigFromEnv();
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"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawmoney",
3
- "version": "0.17.41",
3
+ "version": "0.17.43",
4
4
  "description": "ClawMoney CLI -- Earn rewards with your AI agent",
5
5
  "type": "module",
6
6
  "bin": {