babyclaw 0.0.0
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/build/cli.js +5 -0
- package/build/commands/config/edit.js +34 -0
- package/build/commands/config/init.js +44 -0
- package/build/commands/config/validate.js +51 -0
- package/build/commands/doctor.js +121 -0
- package/build/commands/gateway/reload.js +23 -0
- package/build/commands/gateway/status.js +38 -0
- package/build/commands/index.js +37 -0
- package/build/commands/model/alias/index.js +27 -0
- package/build/commands/model/alias/remove.js +52 -0
- package/build/commands/model/alias/set.js +63 -0
- package/build/commands/model/configure.js +154 -0
- package/build/commands/model/index.js +55 -0
- package/build/commands/pinch.js +34 -0
- package/build/commands/service/install.js +33 -0
- package/build/commands/service/restart.js +30 -0
- package/build/commands/service/start.js +37 -0
- package/build/commands/service/status.js +42 -0
- package/build/commands/service/stop.js +32 -0
- package/build/commands/service/uninstall.js +28 -0
- package/build/commands/setup.js +311 -0
- package/build/commands/skills/install.js +85 -0
- package/build/commands/skills/search.js +114 -0
- package/build/service/adapter.js +265 -0
- package/build/service/adapter.test.js +33 -0
- package/build/ui/theme.js +69 -0
- package/build/ui/theme.test.js +33 -0
- package/package.json +44 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { command } from "@gud/cli";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { loadConfigRaw, installSkillFromClawHub, runSkillSetup, resolveLanguageModel, SkillAlreadyInstalledError, ClawHubError, } from "@babyclaw/gateway";
|
|
4
|
+
import { c } from "../../ui/theme.js";
|
|
5
|
+
export default command({
|
|
6
|
+
description: "Install a skill from ClawHub",
|
|
7
|
+
options: {
|
|
8
|
+
slug: {
|
|
9
|
+
type: "string",
|
|
10
|
+
description: "Skill slug on ClawHub (e.g. gcalcli-calendar)",
|
|
11
|
+
required: true,
|
|
12
|
+
},
|
|
13
|
+
version: {
|
|
14
|
+
type: "string",
|
|
15
|
+
description: "Install a specific version (defaults to latest)",
|
|
16
|
+
},
|
|
17
|
+
force: {
|
|
18
|
+
type: "boolean",
|
|
19
|
+
description: "Overwrite if the skill is already installed",
|
|
20
|
+
},
|
|
21
|
+
"skip-setup": {
|
|
22
|
+
type: "boolean",
|
|
23
|
+
description: "Skip automatic dependency setup after installation",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
handler: async ({ options, client }) => {
|
|
27
|
+
const slug = await options.slug({ prompt: "Skill slug (e.g. gcalcli-calendar)" });
|
|
28
|
+
const version = await options.version();
|
|
29
|
+
const force = await options.force();
|
|
30
|
+
const skipSetup = await options["skip-setup"]();
|
|
31
|
+
try {
|
|
32
|
+
const config = await loadConfigRaw();
|
|
33
|
+
const workspacePath = resolve(process.cwd(), config?.workspace?.root ?? ".");
|
|
34
|
+
const installResult = await installSkillFromClawHub({
|
|
35
|
+
slug,
|
|
36
|
+
version: version || undefined,
|
|
37
|
+
workspacePath,
|
|
38
|
+
force: force || false,
|
|
39
|
+
});
|
|
40
|
+
let setupResult = null;
|
|
41
|
+
let setupError = null;
|
|
42
|
+
if (!skipSetup && config) {
|
|
43
|
+
try {
|
|
44
|
+
const model = resolveLanguageModel({ config });
|
|
45
|
+
setupResult = await runSkillSetup({
|
|
46
|
+
model,
|
|
47
|
+
skillPath: installResult.skillPath,
|
|
48
|
+
workspacePath,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
setupError = err instanceof Error ? err.message : String(err);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
client.log(c.success("✓ Installed ") +
|
|
56
|
+
c.bold(installResult.displayName) +
|
|
57
|
+
c.muted(` (${installResult.version})`));
|
|
58
|
+
client.log(c.muted(` ${installResult.files.length} file${installResult.files.length !== 1 ? "s" : ""} → ${installResult.skillPath}`));
|
|
59
|
+
if (setupResult && !setupResult.skipped) {
|
|
60
|
+
client.log(c.success(" ✓ Dependencies set up"));
|
|
61
|
+
}
|
|
62
|
+
if (setupError) {
|
|
63
|
+
client.log(c.warning(" ⚠ Setup failed (skill files are still installed)"));
|
|
64
|
+
client.log(c.muted(` ${setupError}`));
|
|
65
|
+
}
|
|
66
|
+
client.log(c.muted(" The skill will be available on the next agent session."));
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (error instanceof SkillAlreadyInstalledError) {
|
|
70
|
+
client.log(c.warning(`⚠ Skill "${slug}" is already installed.`));
|
|
71
|
+
client.log(c.muted(" Use ") + c.info("--force") + c.muted(" to overwrite."));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (error instanceof ClawHubError && error.statusCode === 404) {
|
|
75
|
+
client.log(c.error(`✗ Skill "${slug}" not found on ClawHub.`));
|
|
76
|
+
client.log(c.muted(" Browse available skills at ") + c.info("https://clawhub.ai/skills"));
|
|
77
|
+
process.exitCode = 1;
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
client.log(c.error(`✗ Failed to install skill "${slug}"`));
|
|
81
|
+
client.log(c.muted(` ${error instanceof Error ? error.message : String(error)}`));
|
|
82
|
+
process.exitCode = 1;
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { command } from "@gud/cli";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { loadConfigRaw, installSkillFromClawHub, runSkillSetup, resolveLanguageModel, SkillAlreadyInstalledError, } from "@babyclaw/gateway";
|
|
4
|
+
import { c } from "../../ui/theme.js";
|
|
5
|
+
const CLAWHUB_SEARCH_URL = "https://clawhub.ai/api/v1/search";
|
|
6
|
+
export default command({
|
|
7
|
+
description: "Search for skills on ClawHub",
|
|
8
|
+
options: {
|
|
9
|
+
query: {
|
|
10
|
+
type: "string",
|
|
11
|
+
description: "Search query (e.g. notion, calendar, git)",
|
|
12
|
+
required: true,
|
|
13
|
+
},
|
|
14
|
+
json: {
|
|
15
|
+
type: "boolean",
|
|
16
|
+
description: "Output raw JSON instead of formatted results",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
handler: async ({ options, client }) => {
|
|
20
|
+
const query = await options.query({ prompt: "Search query" });
|
|
21
|
+
const json = await options.json();
|
|
22
|
+
const url = `${CLAWHUB_SEARCH_URL}?q=${encodeURIComponent(query)}`;
|
|
23
|
+
let data;
|
|
24
|
+
try {
|
|
25
|
+
const res = await fetch(url);
|
|
26
|
+
if (!res.ok) {
|
|
27
|
+
client.log(c.error(`✗ ClawHub API returned ${res.status}`));
|
|
28
|
+
process.exitCode = 1;
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
data = (await res.json());
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
client.log(c.error("✗ Failed to reach ClawHub API"));
|
|
35
|
+
client.log(c.muted(` ${error instanceof Error ? error.message : String(error)}`));
|
|
36
|
+
process.exitCode = 1;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (data.results.length === 0) {
|
|
40
|
+
client.log(c.warning(`No skills found for "${query}".`));
|
|
41
|
+
client.log(c.muted(" Browse all skills at ") + c.info("https://clawhub.ai/skills"));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (json) {
|
|
45
|
+
client.log(JSON.stringify(data.results, null, 2));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
client.log(c.bold(` 🔍 ${data.results.length} result${data.results.length !== 1 ? "s" : ""} for "${query}"`) + "\n");
|
|
49
|
+
const choices = data.results.map((r) => ({
|
|
50
|
+
title: `${r.displayName} ${c.muted("v" + r.version)} — ${r.summary}`,
|
|
51
|
+
value: r.slug,
|
|
52
|
+
}));
|
|
53
|
+
choices.push({ title: c.muted("Cancel"), value: "__cancel__" });
|
|
54
|
+
const selected = await client.prompt({
|
|
55
|
+
type: "select",
|
|
56
|
+
message: "Select a skill to install",
|
|
57
|
+
choices,
|
|
58
|
+
});
|
|
59
|
+
if (selected === "__cancel__") {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const slug = selected;
|
|
63
|
+
const skill = data.results.find((r) => r.slug === slug);
|
|
64
|
+
client.log("");
|
|
65
|
+
try {
|
|
66
|
+
const config = await loadConfigRaw();
|
|
67
|
+
const workspacePath = resolve(process.cwd(), config?.workspace?.root ?? ".");
|
|
68
|
+
const installResult = await installSkillFromClawHub({
|
|
69
|
+
slug,
|
|
70
|
+
workspacePath,
|
|
71
|
+
force: false,
|
|
72
|
+
});
|
|
73
|
+
let setupResult = null;
|
|
74
|
+
let setupError = null;
|
|
75
|
+
if (config) {
|
|
76
|
+
try {
|
|
77
|
+
const model = resolveLanguageModel({ config });
|
|
78
|
+
setupResult = await runSkillSetup({
|
|
79
|
+
model,
|
|
80
|
+
skillPath: installResult.skillPath,
|
|
81
|
+
workspacePath,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
setupError = err instanceof Error ? err.message : String(err);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
client.log(c.success("✓ Installed ") +
|
|
89
|
+
c.bold(installResult.displayName) +
|
|
90
|
+
c.muted(` (${installResult.version})`));
|
|
91
|
+
client.log(c.muted(` ${installResult.files.length} file${installResult.files.length !== 1 ? "s" : ""} → ${installResult.skillPath}`));
|
|
92
|
+
if (setupResult && !setupResult.skipped) {
|
|
93
|
+
client.log(c.success(" ✓ Dependencies set up"));
|
|
94
|
+
}
|
|
95
|
+
if (setupError) {
|
|
96
|
+
client.log(c.warning(" ⚠ Setup failed (skill files are still installed)"));
|
|
97
|
+
client.log(c.muted(` ${setupError}`));
|
|
98
|
+
}
|
|
99
|
+
client.log(c.muted(" The skill will be available on the next agent session."));
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (error instanceof SkillAlreadyInstalledError) {
|
|
103
|
+
client.log(c.warning(`⚠ Skill "${skill.displayName}" is already installed.`));
|
|
104
|
+
client.log(c.muted(" Use ") +
|
|
105
|
+
c.info(`babyclaw skill install --slug ${slug} --force`) +
|
|
106
|
+
c.muted(" to overwrite."));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
client.log(c.error(`✗ Failed to install "${skill.displayName}"`));
|
|
110
|
+
client.log(c.muted(` ${error instanceof Error ? error.message : String(error)}`));
|
|
111
|
+
process.exitCode = 1;
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
});
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { platform } from "node:os";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { join, dirname } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { mkdirSync, writeFileSync, unlinkSync } from "node:fs";
|
|
8
|
+
import { createRequire } from "node:module";
|
|
9
|
+
const SERVICE_LABEL = "org.babyclaw.gateway";
|
|
10
|
+
const SYSTEMD_UNIT = "babyclaw-gateway";
|
|
11
|
+
export function detectPlatform() {
|
|
12
|
+
const os = platform();
|
|
13
|
+
if (os === "darwin")
|
|
14
|
+
return "launchd";
|
|
15
|
+
if (os === "linux")
|
|
16
|
+
return "systemd";
|
|
17
|
+
return "unsupported";
|
|
18
|
+
}
|
|
19
|
+
function getLaunchdPlistPath() {
|
|
20
|
+
return join(homedir(), "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`);
|
|
21
|
+
}
|
|
22
|
+
function getSystemdUnitPath() {
|
|
23
|
+
return join(homedir(), ".config", "systemd", "user", `${SYSTEMD_UNIT}.service`);
|
|
24
|
+
}
|
|
25
|
+
function getGatewayEntryPath() {
|
|
26
|
+
try {
|
|
27
|
+
const require = createRequire(import.meta.url);
|
|
28
|
+
return require.resolve("@babyclaw/gateway/dist/main.js");
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
32
|
+
return join(thisDir, "..", "..", "..", "gateway", "dist", "main.js");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function getShellPath() {
|
|
36
|
+
const fallback = `/usr/local/bin:/usr/bin:/bin`;
|
|
37
|
+
try {
|
|
38
|
+
const shell = process.env.SHELL || "/bin/zsh";
|
|
39
|
+
return execSync(`${shell} -ilc 'echo $PATH'`, {
|
|
40
|
+
encoding: "utf8",
|
|
41
|
+
timeout: 5000,
|
|
42
|
+
}).trim();
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return process.env.PATH || fallback;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function generateLaunchdPlist() {
|
|
49
|
+
const nodePath = execSync("which node", { encoding: "utf8" }).trim();
|
|
50
|
+
const entryPath = getGatewayEntryPath();
|
|
51
|
+
const logDir = join(homedir(), ".babyclaw", "logs");
|
|
52
|
+
const shellPath = getShellPath();
|
|
53
|
+
const nodeDir = dirname(nodePath);
|
|
54
|
+
const path = shellPath.includes(nodeDir) ? shellPath : `${shellPath}:${nodeDir}`;
|
|
55
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
56
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
57
|
+
<plist version="1.0">
|
|
58
|
+
<dict>
|
|
59
|
+
<key>Label</key>
|
|
60
|
+
<string>${SERVICE_LABEL}</string>
|
|
61
|
+
<key>ProgramArguments</key>
|
|
62
|
+
<array>
|
|
63
|
+
<string>${nodePath}</string>
|
|
64
|
+
<string>${entryPath}</string>
|
|
65
|
+
</array>
|
|
66
|
+
<key>RunAtLoad</key>
|
|
67
|
+
<true/>
|
|
68
|
+
<key>KeepAlive</key>
|
|
69
|
+
<true/>
|
|
70
|
+
<key>StandardOutPath</key>
|
|
71
|
+
<string>${logDir}/gateway.stdout.log</string>
|
|
72
|
+
<key>StandardErrorPath</key>
|
|
73
|
+
<string>${logDir}/gateway.stderr.log</string>
|
|
74
|
+
<key>EnvironmentVariables</key>
|
|
75
|
+
<dict>
|
|
76
|
+
<key>PATH</key>
|
|
77
|
+
<string>${path}</string>
|
|
78
|
+
</dict>
|
|
79
|
+
</dict>
|
|
80
|
+
</plist>`;
|
|
81
|
+
}
|
|
82
|
+
function generateSystemdUnit() {
|
|
83
|
+
const nodePath = execSync("which node", { encoding: "utf8" }).trim();
|
|
84
|
+
const entryPath = getGatewayEntryPath();
|
|
85
|
+
return `[Unit]
|
|
86
|
+
Description=BabyClaw Gateway
|
|
87
|
+
After=network.target
|
|
88
|
+
|
|
89
|
+
[Service]
|
|
90
|
+
Type=simple
|
|
91
|
+
ExecStart=${nodePath} ${entryPath}
|
|
92
|
+
Restart=on-failure
|
|
93
|
+
RestartSec=5
|
|
94
|
+
|
|
95
|
+
[Install]
|
|
96
|
+
WantedBy=default.target`;
|
|
97
|
+
}
|
|
98
|
+
export function install() {
|
|
99
|
+
const plat = detectPlatform();
|
|
100
|
+
if (plat === "launchd") {
|
|
101
|
+
const plistPath = getLaunchdPlistPath();
|
|
102
|
+
mkdirSync(dirname(plistPath), { recursive: true });
|
|
103
|
+
mkdirSync(join(homedir(), ".babyclaw", "logs"), { recursive: true });
|
|
104
|
+
writeFileSync(plistPath, generateLaunchdPlist(), "utf8");
|
|
105
|
+
return { path: plistPath };
|
|
106
|
+
}
|
|
107
|
+
if (plat === "systemd") {
|
|
108
|
+
const unitPath = getSystemdUnitPath();
|
|
109
|
+
mkdirSync(dirname(unitPath), { recursive: true });
|
|
110
|
+
writeFileSync(unitPath, generateSystemdUnit(), "utf8");
|
|
111
|
+
execSync("systemctl --user daemon-reload");
|
|
112
|
+
execSync(`systemctl --user enable ${SYSTEMD_UNIT}`);
|
|
113
|
+
return { path: unitPath };
|
|
114
|
+
}
|
|
115
|
+
throw new Error("Unsupported platform for service installation");
|
|
116
|
+
}
|
|
117
|
+
export function uninstall() {
|
|
118
|
+
const plat = detectPlatform();
|
|
119
|
+
if (plat === "launchd") {
|
|
120
|
+
const plistPath = getLaunchdPlistPath();
|
|
121
|
+
try {
|
|
122
|
+
execSync(`launchctl bootout gui/$(id -u) ${plistPath} 2>/dev/null`);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// Already unloaded
|
|
126
|
+
}
|
|
127
|
+
if (existsSync(plistPath)) {
|
|
128
|
+
unlinkSync(plistPath);
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (plat === "systemd") {
|
|
133
|
+
const unitPath = getSystemdUnitPath();
|
|
134
|
+
try {
|
|
135
|
+
execSync(`systemctl --user stop ${SYSTEMD_UNIT} 2>/dev/null`);
|
|
136
|
+
execSync(`systemctl --user disable ${SYSTEMD_UNIT} 2>/dev/null`);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// Already stopped/disabled
|
|
140
|
+
}
|
|
141
|
+
if (existsSync(unitPath)) {
|
|
142
|
+
unlinkSync(unitPath);
|
|
143
|
+
}
|
|
144
|
+
execSync("systemctl --user daemon-reload");
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
throw new Error("Unsupported platform for service uninstallation");
|
|
148
|
+
}
|
|
149
|
+
export function start() {
|
|
150
|
+
const plat = detectPlatform();
|
|
151
|
+
if (plat === "launchd") {
|
|
152
|
+
const plistPath = getLaunchdPlistPath();
|
|
153
|
+
if (!existsSync(plistPath)) {
|
|
154
|
+
throw new Error("Service not installed. Run 'babyclaw service install' first.");
|
|
155
|
+
}
|
|
156
|
+
execSync(`launchctl bootstrap gui/$(id -u) ${plistPath}`);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (plat === "systemd") {
|
|
160
|
+
execSync(`systemctl --user start ${SYSTEMD_UNIT}`);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
throw new Error("Unsupported platform");
|
|
164
|
+
}
|
|
165
|
+
export function stop() {
|
|
166
|
+
const plat = detectPlatform();
|
|
167
|
+
if (plat === "launchd") {
|
|
168
|
+
const plistPath = getLaunchdPlistPath();
|
|
169
|
+
execSync(`launchctl bootout gui/$(id -u) ${plistPath}`);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (plat === "systemd") {
|
|
173
|
+
execSync(`systemctl --user stop ${SYSTEMD_UNIT}`);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
throw new Error("Unsupported platform");
|
|
177
|
+
}
|
|
178
|
+
export function restart() {
|
|
179
|
+
const plat = detectPlatform();
|
|
180
|
+
if (plat === "launchd") {
|
|
181
|
+
const plistPath = getLaunchdPlistPath();
|
|
182
|
+
try {
|
|
183
|
+
execSync(`launchctl bootout gui/$(id -u) ${plistPath} 2>/dev/null`);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
// May not be running
|
|
187
|
+
}
|
|
188
|
+
execSync(`launchctl bootstrap gui/$(id -u) ${plistPath}`);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (plat === "systemd") {
|
|
192
|
+
execSync(`systemctl --user restart ${SYSTEMD_UNIT}`);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
throw new Error("Unsupported platform");
|
|
196
|
+
}
|
|
197
|
+
export function getStatus() {
|
|
198
|
+
const plat = detectPlatform();
|
|
199
|
+
if (plat === "launchd") {
|
|
200
|
+
const plistPath = getLaunchdPlistPath();
|
|
201
|
+
const installed = existsSync(plistPath);
|
|
202
|
+
let running = false;
|
|
203
|
+
let pid = null;
|
|
204
|
+
if (installed) {
|
|
205
|
+
try {
|
|
206
|
+
const output = execSync(`launchctl list ${SERVICE_LABEL} 2>/dev/null`, {
|
|
207
|
+
encoding: "utf8",
|
|
208
|
+
});
|
|
209
|
+
running = true;
|
|
210
|
+
const pidMatch = output.match(/"PID"\s*=\s*(\d+)/);
|
|
211
|
+
if (pidMatch) {
|
|
212
|
+
pid = parseInt(pidMatch[1], 10);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
// Service loaded but not running, or not loaded
|
|
217
|
+
try {
|
|
218
|
+
const output = execSync(`launchctl print gui/$(id -u)/${SERVICE_LABEL} 2>/dev/null`, {
|
|
219
|
+
encoding: "utf8",
|
|
220
|
+
});
|
|
221
|
+
running = output.includes("state = running");
|
|
222
|
+
const pidMatch = output.match(/pid\s*=\s*(\d+)/);
|
|
223
|
+
if (pidMatch) {
|
|
224
|
+
pid = parseInt(pidMatch[1], 10);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
running = false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return { installed, running, pid, platform: "launchd" };
|
|
233
|
+
}
|
|
234
|
+
if (plat === "systemd") {
|
|
235
|
+
const unitPath = getSystemdUnitPath();
|
|
236
|
+
const installed = existsSync(unitPath);
|
|
237
|
+
let running = false;
|
|
238
|
+
let pid = null;
|
|
239
|
+
if (installed) {
|
|
240
|
+
try {
|
|
241
|
+
const output = execSync(`systemctl --user is-active ${SYSTEMD_UNIT} 2>/dev/null`, {
|
|
242
|
+
encoding: "utf8",
|
|
243
|
+
});
|
|
244
|
+
running = output.trim() === "active";
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
running = false;
|
|
248
|
+
}
|
|
249
|
+
if (running) {
|
|
250
|
+
try {
|
|
251
|
+
const output = execSync(`systemctl --user show ${SYSTEMD_UNIT} --property=MainPID 2>/dev/null`, { encoding: "utf8" });
|
|
252
|
+
const match = output.match(/MainPID=(\d+)/);
|
|
253
|
+
if (match && match[1] !== "0") {
|
|
254
|
+
pid = parseInt(match[1], 10);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
// Ignore
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return { installed, running, pid, platform: "systemd" };
|
|
263
|
+
}
|
|
264
|
+
return { installed: false, running: false, pid: null, platform: "unsupported" };
|
|
265
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { detectPlatform, getStatus } from "./adapter.js";
|
|
3
|
+
import { platform } from "node:os";
|
|
4
|
+
describe("service adapter", () => {
|
|
5
|
+
it("detects the correct platform", () => {
|
|
6
|
+
const os = platform();
|
|
7
|
+
const detected = detectPlatform();
|
|
8
|
+
if (os === "darwin") {
|
|
9
|
+
expect(detected).toBe("launchd");
|
|
10
|
+
}
|
|
11
|
+
else if (os === "linux") {
|
|
12
|
+
expect(detected).toBe("systemd");
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
expect(detected).toBe("unsupported");
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
it("returns a ServiceInfo object from getStatus", () => {
|
|
19
|
+
const info = getStatus();
|
|
20
|
+
expect(info).toHaveProperty("installed");
|
|
21
|
+
expect(info).toHaveProperty("running");
|
|
22
|
+
expect(info).toHaveProperty("pid");
|
|
23
|
+
expect(info).toHaveProperty("platform");
|
|
24
|
+
expect(typeof info.installed).toBe("boolean");
|
|
25
|
+
expect(typeof info.running).toBe("boolean");
|
|
26
|
+
});
|
|
27
|
+
it("reports not installed when no service file exists", () => {
|
|
28
|
+
const info = getStatus();
|
|
29
|
+
// On a clean dev machine, service should not be installed
|
|
30
|
+
// This test verifies the adapter doesn't crash when querying
|
|
31
|
+
expect(typeof info.installed).toBe("boolean");
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
export const c = {
|
|
3
|
+
brand: pc.magenta,
|
|
4
|
+
success: pc.green,
|
|
5
|
+
error: pc.red,
|
|
6
|
+
warning: pc.yellow,
|
|
7
|
+
muted: pc.gray,
|
|
8
|
+
info: pc.blue,
|
|
9
|
+
bold: pc.bold,
|
|
10
|
+
};
|
|
11
|
+
const BANNERS = [
|
|
12
|
+
`
|
|
13
|
+
╭─────────────────────────────╮
|
|
14
|
+
│ │
|
|
15
|
+
│ 🦀 babyclaw v1.0.0 │
|
|
16
|
+
│ │
|
|
17
|
+
│ your friendly agent │
|
|
18
|
+
│ gateway, at your service │
|
|
19
|
+
│ │
|
|
20
|
+
╰─────────────────────────────╯`,
|
|
21
|
+
`
|
|
22
|
+
╭─────────────────────────────╮
|
|
23
|
+
│ │
|
|
24
|
+
│ 🦞 babyclaw v1.0.0 │
|
|
25
|
+
│ │
|
|
26
|
+
│ pincers ready, │
|
|
27
|
+
│ tasks loaded. │
|
|
28
|
+
│ │
|
|
29
|
+
╰─────────────────────────────╯`,
|
|
30
|
+
`
|
|
31
|
+
╭─────────────────────────────╮
|
|
32
|
+
│ │
|
|
33
|
+
│ 🦀 babyclaw v1.0.0 │
|
|
34
|
+
│ │
|
|
35
|
+
│ snip snip. │
|
|
36
|
+
│ let's get to work. │
|
|
37
|
+
│ │
|
|
38
|
+
╰─────────────────────────────╯`,
|
|
39
|
+
];
|
|
40
|
+
export function getRandomBanner() {
|
|
41
|
+
const index = Math.floor(Math.random() * BANNERS.length);
|
|
42
|
+
return BANNERS[index];
|
|
43
|
+
}
|
|
44
|
+
export function formatUptime(ms) {
|
|
45
|
+
const seconds = Math.floor(ms / 1000);
|
|
46
|
+
if (seconds < 60)
|
|
47
|
+
return `${seconds}s`;
|
|
48
|
+
const minutes = Math.floor(seconds / 60);
|
|
49
|
+
if (minutes < 60)
|
|
50
|
+
return `${minutes}m ${seconds % 60}s`;
|
|
51
|
+
const hours = Math.floor(minutes / 60);
|
|
52
|
+
if (hours < 24)
|
|
53
|
+
return `${hours}h ${minutes % 60}m`;
|
|
54
|
+
const days = Math.floor(hours / 24);
|
|
55
|
+
return `${days}d ${hours % 24}h`;
|
|
56
|
+
}
|
|
57
|
+
const TIPS = [
|
|
58
|
+
"Run 'babyclaw config edit' to tweak your setup interactively.",
|
|
59
|
+
"Use 'babyclaw service status' to check if the gateway is alive.",
|
|
60
|
+
"Add '--json' to most commands for machine-readable output.",
|
|
61
|
+
"The gateway talks over a local Unix socket — no ports to configure.",
|
|
62
|
+
"Use 'babyclaw config validate' after manual edits to catch typos.",
|
|
63
|
+
"Your config lives at ~/.babyclaw/babyclaw.json by default.",
|
|
64
|
+
"You can override the config path with BABYCLAW_CONFIG_PATH.",
|
|
65
|
+
];
|
|
66
|
+
export function getRandomTip() {
|
|
67
|
+
const index = Math.floor(Math.random() * TIPS.length);
|
|
68
|
+
return TIPS[index];
|
|
69
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { formatUptime, getRandomBanner, getRandomTip } from "./theme.js";
|
|
3
|
+
describe("formatUptime", () => {
|
|
4
|
+
it("formats seconds", () => {
|
|
5
|
+
expect(formatUptime(5000)).toBe("5s");
|
|
6
|
+
expect(formatUptime(59000)).toBe("59s");
|
|
7
|
+
});
|
|
8
|
+
it("formats minutes and seconds", () => {
|
|
9
|
+
expect(formatUptime(90_000)).toBe("1m 30s");
|
|
10
|
+
expect(formatUptime(3_540_000)).toBe("59m 0s");
|
|
11
|
+
});
|
|
12
|
+
it("formats hours and minutes", () => {
|
|
13
|
+
expect(formatUptime(3_600_000)).toBe("1h 0m");
|
|
14
|
+
expect(formatUptime(7_260_000)).toBe("2h 1m");
|
|
15
|
+
});
|
|
16
|
+
it("formats days and hours", () => {
|
|
17
|
+
expect(formatUptime(86_400_000)).toBe("1d 0h");
|
|
18
|
+
expect(formatUptime(100_000_000)).toBe("1d 3h");
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
describe("getRandomBanner", () => {
|
|
22
|
+
it("returns a non-empty string", () => {
|
|
23
|
+
const banner = getRandomBanner();
|
|
24
|
+
expect(banner.length).toBeGreaterThan(0);
|
|
25
|
+
expect(banner).toContain("babyclaw");
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
describe("getRandomTip", () => {
|
|
29
|
+
it("returns a non-empty string", () => {
|
|
30
|
+
const tip = getRandomTip();
|
|
31
|
+
expect(tip.length).toBeGreaterThan(0);
|
|
32
|
+
});
|
|
33
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "babyclaw",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "The babyclaw command-line interface",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"agent",
|
|
7
|
+
"babyclaw",
|
|
8
|
+
"cli",
|
|
9
|
+
"gateway"
|
|
10
|
+
],
|
|
11
|
+
"license": "ISC",
|
|
12
|
+
"bin": {
|
|
13
|
+
"babyclaw": "./build/cli.js"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"build"
|
|
17
|
+
],
|
|
18
|
+
"type": "module",
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@gud/cli": "1.0.0-beta.7",
|
|
21
|
+
"picocolors": "^1.1.1",
|
|
22
|
+
"@babyclaw/gateway": "0.0.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/node": "^24.3.0",
|
|
26
|
+
"oxlint": "^1.49.0",
|
|
27
|
+
"typescript": "^5.9.2",
|
|
28
|
+
"vitest": "^4.0.18"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=20"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsc",
|
|
35
|
+
"dev": "tsc --watch",
|
|
36
|
+
"typecheck": "tsc --noEmit",
|
|
37
|
+
"lint": "oxlint",
|
|
38
|
+
"lint:fix": "oxlint --fix",
|
|
39
|
+
"format": "oxfmt --write .",
|
|
40
|
+
"format:check": "oxfmt --check .",
|
|
41
|
+
"test": "vitest run",
|
|
42
|
+
"test:watch": "vitest"
|
|
43
|
+
}
|
|
44
|
+
}
|