@harness.farm/clawfirm 0.1.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/bin/cli.js +105 -0
- package/clawfirm.config.js +44 -0
- package/lib/auth.js +59 -0
- package/lib/dispatch.js +13 -0
- package/lib/install.js +134 -0
- package/lib/login.js +138 -0
- package/lib/new.js +117 -0
- package/lib/skills.js +49 -0
- package/package.json +34 -0
package/bin/cli.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { resolve, dirname } from "node:path";
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
|
|
8
|
+
async function loadConfig() {
|
|
9
|
+
const configPath = resolve(__dirname, "../clawfirm.config.js");
|
|
10
|
+
const { default: config } = await import(configPath);
|
|
11
|
+
return config;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function printHelp(config) {
|
|
15
|
+
console.log(`
|
|
16
|
+
clawfirm — dev tool manager
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
clawfirm login Log in to clawfirm.dev
|
|
20
|
+
clawfirm whoami Show current session
|
|
21
|
+
clawfirm logout Log out
|
|
22
|
+
clawfirm new "<description>" Start a project from natural language
|
|
23
|
+
clawfirm install [tool] Install all tools (or a specific one)
|
|
24
|
+
clawfirm uninstall [tool] Uninstall all tools (or a specific one)
|
|
25
|
+
clawfirm list List registered tools
|
|
26
|
+
clawfirm <name> [args] Dispatch to clawfirm-<name>
|
|
27
|
+
clawfirm help Show this message
|
|
28
|
+
|
|
29
|
+
Tools in registry:
|
|
30
|
+
${config.tools.map((t) => ` ${t.name.padEnd(14)} ${t.description}`).join("\n")}
|
|
31
|
+
`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function printList(config) {
|
|
35
|
+
console.log(`\n Registered tools:\n`);
|
|
36
|
+
for (const t of config.tools) {
|
|
37
|
+
console.log(` ${t.name.padEnd(14)} ${t.description}`);
|
|
38
|
+
console.log(` ${"".padEnd(14)} ${t.homepage}\n`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function main() {
|
|
43
|
+
const [, , command, ...rest] = process.argv;
|
|
44
|
+
|
|
45
|
+
const config = await loadConfig();
|
|
46
|
+
|
|
47
|
+
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
48
|
+
printHelp(config);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (command === "list") {
|
|
53
|
+
printList(config);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (command === "login") {
|
|
58
|
+
const { runLogin } = await import("../lib/login.js");
|
|
59
|
+
await runLogin();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (command === "whoami") {
|
|
64
|
+
const { runWhoami } = await import("../lib/login.js");
|
|
65
|
+
await runWhoami();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (command === "logout") {
|
|
70
|
+
const { runLogout } = await import("../lib/login.js");
|
|
71
|
+
await runLogout();
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Commands below require a valid license
|
|
76
|
+
const { checkLicense } = await import("../lib/auth.js");
|
|
77
|
+
checkLicense();
|
|
78
|
+
|
|
79
|
+
if (command === "new") {
|
|
80
|
+
const { runNew } = await import("../lib/new.js");
|
|
81
|
+
await runNew(rest.join(" "));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (command === "install") {
|
|
86
|
+
const { runInstall } = await import("../lib/install.js");
|
|
87
|
+
await runInstall(config, rest[0]);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (command === "uninstall") {
|
|
92
|
+
const { runUninstall } = await import("../lib/install.js");
|
|
93
|
+
await runUninstall(config, rest[0]);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Plugin dispatch: clawfirm <name> → clawfirm-<name>
|
|
98
|
+
const { dispatch } = await import("../lib/dispatch.js");
|
|
99
|
+
dispatch(command, rest);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
main().catch((err) => {
|
|
103
|
+
console.error(err.message);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* clawfirm tool registry
|
|
3
|
+
* Each tool needs: name, install command, and the binary it provides.
|
|
4
|
+
*/
|
|
5
|
+
export default {
|
|
6
|
+
tools: [
|
|
7
|
+
{
|
|
8
|
+
name: "openvault",
|
|
9
|
+
description: "Encrypted local secret manager",
|
|
10
|
+
requires: "go",
|
|
11
|
+
install: "go install github.com/npc-live/openvault@latest",
|
|
12
|
+
uninstall: "rm -f $(which openvault)",
|
|
13
|
+
check: "openvault",
|
|
14
|
+
homepage: "https://openvault.sh",
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: "skillctl",
|
|
18
|
+
description: "Sync AI skills across coding tools",
|
|
19
|
+
requires: "npm",
|
|
20
|
+
install: "npm install -g @harness.farm/skillctl",
|
|
21
|
+
uninstall: "npm uninstall -g @harness.farm/skillctl",
|
|
22
|
+
check: "skillctl",
|
|
23
|
+
homepage: "https://skillctl.dev",
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: "whipflow",
|
|
27
|
+
description: "Deterministic AI workflow runner",
|
|
28
|
+
requires: "npm",
|
|
29
|
+
install: "npm install -g @harness.farm/whipflow",
|
|
30
|
+
uninstall: "npm uninstall -g @harness.farm/whipflow",
|
|
31
|
+
check: "whipflow",
|
|
32
|
+
homepage: "https://whipflow.dev",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: "agent-browser",
|
|
36
|
+
description: "Browser automation for AI agents",
|
|
37
|
+
requires: "npm",
|
|
38
|
+
install: "npm install -g agent-browser",
|
|
39
|
+
uninstall: "npm uninstall -g agent-browser",
|
|
40
|
+
check: "agent-browser",
|
|
41
|
+
homepage: "https://www.npmjs.com/package/agent-browser",
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
};
|
package/lib/auth.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
const CONFIG_DIR = join(homedir(), ".clawfirm");
|
|
6
|
+
const SESSION_FILE = join(CONFIG_DIR, "session.json");
|
|
7
|
+
|
|
8
|
+
export const BASE_URL = "https://clawfirm.dev";
|
|
9
|
+
|
|
10
|
+
function ensureDir() {
|
|
11
|
+
if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function saveSession(data) {
|
|
15
|
+
ensureDir();
|
|
16
|
+
writeFileSync(SESSION_FILE, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function loadSession() {
|
|
20
|
+
if (!existsSync(SESSION_FILE)) return null;
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(readFileSync(SESSION_FILE, "utf8"));
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function clearSession() {
|
|
29
|
+
if (existsSync(SESSION_FILE)) unlinkSync(SESSION_FILE);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function requireSession() {
|
|
33
|
+
const session = loadSession();
|
|
34
|
+
if (!session) {
|
|
35
|
+
console.error("\n Not logged in. Run: clawfirm login\n");
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
return session;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function checkLicense() {
|
|
42
|
+
const session = loadSession();
|
|
43
|
+
|
|
44
|
+
if (!session) {
|
|
45
|
+
console.error("\n Not logged in. Run: clawfirm login\n");
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!session.unlocked || !session.unlockedUntil) {
|
|
50
|
+
console.error("\n Account not activated. Visit https://clawfirm.dev\n");
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const expiry = new Date(session.unlockedUntil);
|
|
55
|
+
if (isNaN(expiry.getTime()) || expiry < new Date()) {
|
|
56
|
+
console.error(`\n License expired (${session.unlockedUntil}). Visit https://clawfirm.dev to renew.\n`);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
}
|
package/lib/dispatch.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export function dispatch(name, args) {
|
|
4
|
+
const cmd = `clawfirm-${name}`;
|
|
5
|
+
const result = spawnSync(cmd, args, { stdio: "inherit" });
|
|
6
|
+
|
|
7
|
+
if (result.error?.code === "ENOENT") {
|
|
8
|
+
console.error(`\n clawfirm: '${name}' not found (tried: ${cmd})\n`);
|
|
9
|
+
process.exit(127);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
process.exit(result.status ?? 0);
|
|
13
|
+
}
|
package/lib/install.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { execSync, spawnSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
const GREEN = "\x1b[32m";
|
|
4
|
+
const YELLOW = "\x1b[33m";
|
|
5
|
+
const RED = "\x1b[31m";
|
|
6
|
+
const BLUE = "\x1b[34m";
|
|
7
|
+
const DIM = "\x1b[2m";
|
|
8
|
+
const NC = "\x1b[0m";
|
|
9
|
+
|
|
10
|
+
const ok = (msg) => console.log(`${GREEN} ✓${NC} ${msg}`);
|
|
11
|
+
const info = (msg) => console.log(`${BLUE} →${NC} ${msg}`);
|
|
12
|
+
const warn = (msg) => console.log(`${YELLOW} !${NC} ${msg}`);
|
|
13
|
+
const fail = (msg) => console.log(`${RED} ✗${NC} ${msg}`);
|
|
14
|
+
|
|
15
|
+
function isInstalled(cmd) {
|
|
16
|
+
const result = spawnSync("which", [cmd], { encoding: "utf8" });
|
|
17
|
+
return result.status === 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function hasRequirement(req) {
|
|
21
|
+
return isInstalled(req);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function installTool(tool) {
|
|
25
|
+
info(`Installing ${tool.name}${DIM} — ${tool.description}${NC}`);
|
|
26
|
+
|
|
27
|
+
if (!hasRequirement(tool.requires)) {
|
|
28
|
+
fail(`${tool.name}: requires '${tool.requires}' but it's not installed`);
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
execSync(tool.install, { stdio: "inherit" });
|
|
34
|
+
ok(`${tool.name} updated`);
|
|
35
|
+
return true;
|
|
36
|
+
} catch {
|
|
37
|
+
fail(`${tool.name}: install failed`);
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function uninstallTool(tool) {
|
|
43
|
+
info(`Uninstalling ${tool.name}${DIM} — ${tool.description}${NC}`);
|
|
44
|
+
|
|
45
|
+
if (!isInstalled(tool.check)) {
|
|
46
|
+
ok(`${tool.name} not installed, skipping`);
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!tool.uninstall) {
|
|
51
|
+
fail(`${tool.name}: no uninstall command defined`);
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
execSync(tool.uninstall, { stdio: "inherit", shell: true });
|
|
57
|
+
ok(`${tool.name} uninstalled`);
|
|
58
|
+
return true;
|
|
59
|
+
} catch {
|
|
60
|
+
fail(`${tool.name}: uninstall failed`);
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function runUninstall(config, target) {
|
|
66
|
+
const tools = target
|
|
67
|
+
? config.tools.filter((t) => t.name === target)
|
|
68
|
+
: config.tools;
|
|
69
|
+
|
|
70
|
+
if (target && tools.length === 0) {
|
|
71
|
+
fail(`Unknown tool: ${target}`);
|
|
72
|
+
console.log(
|
|
73
|
+
`\n Available: ${config.tools.map((t) => t.name).join(", ")}\n`
|
|
74
|
+
);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
console.log(`\n clawfirm uninstall\n`);
|
|
79
|
+
|
|
80
|
+
const failed = [];
|
|
81
|
+
for (const tool of tools) {
|
|
82
|
+
const success = uninstallTool(tool);
|
|
83
|
+
if (!success) failed.push(tool.name);
|
|
84
|
+
console.log();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (failed.length > 0) {
|
|
88
|
+
warn(`Failed: ${failed.join(", ")}`);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
console.log(` Done.\n`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function runInstall(config, target) {
|
|
96
|
+
const tools = target
|
|
97
|
+
? config.tools.filter((t) => t.name === target)
|
|
98
|
+
: config.tools;
|
|
99
|
+
|
|
100
|
+
if (target && tools.length === 0) {
|
|
101
|
+
fail(`Unknown tool: ${target}`);
|
|
102
|
+
console.log(
|
|
103
|
+
`\n Available: ${config.tools.map((t) => t.name).join(", ")}\n`
|
|
104
|
+
);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
console.log(`\n clawfirm install\n`);
|
|
109
|
+
|
|
110
|
+
const failed = [];
|
|
111
|
+
for (const tool of tools) {
|
|
112
|
+
const ok = installTool(tool);
|
|
113
|
+
if (!ok) failed.push(tool.name);
|
|
114
|
+
console.log();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (failed.length > 0) {
|
|
118
|
+
warn(`Failed: ${failed.join(", ")}`);
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Download encrypted skills (stored as .enc, not yet decrypted)
|
|
123
|
+
if (!target) {
|
|
124
|
+
const { loadSession } = await import("./auth.js");
|
|
125
|
+
const { installSkills } = await import("./skills.js");
|
|
126
|
+
const session = loadSession();
|
|
127
|
+
if (session) {
|
|
128
|
+
await installSkills(session);
|
|
129
|
+
console.log();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
console.log(` All tools ready.\n`);
|
|
134
|
+
}
|
package/lib/login.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { createInterface } from "node:readline";
|
|
2
|
+
import { BASE_URL, saveSession, clearSession, loadSession } from "./auth.js";
|
|
3
|
+
|
|
4
|
+
const GREEN = "\x1b[32m";
|
|
5
|
+
const YELLOW = "\x1b[33m";
|
|
6
|
+
const RED = "\x1b[31m";
|
|
7
|
+
const BLUE = "\x1b[34m";
|
|
8
|
+
const DIM = "\x1b[2m";
|
|
9
|
+
const NC = "\x1b[0m";
|
|
10
|
+
|
|
11
|
+
const ok = (msg) => console.log(`${GREEN} ✓${NC} ${msg}`);
|
|
12
|
+
const info = (msg) => console.log(`${BLUE} →${NC} ${msg}`);
|
|
13
|
+
const fail = (msg) => console.log(`${RED} ✗${NC} ${msg}`);
|
|
14
|
+
|
|
15
|
+
function ask(question) {
|
|
16
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
17
|
+
return new Promise((resolve) => {
|
|
18
|
+
rl.question(question, (ans) => { rl.close(); resolve(ans.trim()); });
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function askPassword(question) {
|
|
23
|
+
return new Promise((resolve) => {
|
|
24
|
+
process.stdout.write(question);
|
|
25
|
+
let password = "";
|
|
26
|
+
|
|
27
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
28
|
+
process.stdin.resume();
|
|
29
|
+
process.stdin.setEncoding("utf8");
|
|
30
|
+
|
|
31
|
+
function onData(char) {
|
|
32
|
+
if (char === "\n" || char === "\r" || char === "\u0004") {
|
|
33
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
34
|
+
process.stdin.pause();
|
|
35
|
+
process.stdin.removeListener("data", onData);
|
|
36
|
+
process.stdout.write("\n");
|
|
37
|
+
resolve(password);
|
|
38
|
+
} else if (char === "\u0003") {
|
|
39
|
+
process.exit();
|
|
40
|
+
} else if (char === "\u007f" || char === "\b") {
|
|
41
|
+
password = password.slice(0, -1);
|
|
42
|
+
} else {
|
|
43
|
+
password += char;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
process.stdin.on("data", onData);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function fetchMe(cookie) {
|
|
52
|
+
const res = await fetch(`${BASE_URL}/api/auth/me`, {
|
|
53
|
+
headers: { Cookie: cookie },
|
|
54
|
+
});
|
|
55
|
+
if (!res.ok) return null;
|
|
56
|
+
return res.json();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function runLogin() {
|
|
60
|
+
console.log(`\n clawfirm login\n`);
|
|
61
|
+
|
|
62
|
+
const email = await ask(" Email: ");
|
|
63
|
+
const password = await askPassword(" Password: ");
|
|
64
|
+
|
|
65
|
+
info("Signing in...");
|
|
66
|
+
|
|
67
|
+
let res;
|
|
68
|
+
try {
|
|
69
|
+
res = await fetch(`${BASE_URL}/api/auth/login`, {
|
|
70
|
+
method: "POST",
|
|
71
|
+
headers: { "Content-Type": "application/json" },
|
|
72
|
+
body: JSON.stringify({ email, password }),
|
|
73
|
+
});
|
|
74
|
+
} catch (e) {
|
|
75
|
+
fail(`Network error: ${e.message}`);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!res.ok) {
|
|
80
|
+
const body = await res.json().catch(() => ({}));
|
|
81
|
+
fail(`Login failed: ${body.error ?? res.statusText}`);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Extract session cookie from Set-Cookie header
|
|
86
|
+
const setCookie = res.headers.get("set-cookie");
|
|
87
|
+
if (!setCookie) {
|
|
88
|
+
fail("No session cookie returned");
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
const cookie = setCookie.split(";")[0];
|
|
92
|
+
|
|
93
|
+
// Fetch user info
|
|
94
|
+
const me = await fetchMe(cookie);
|
|
95
|
+
if (!me) {
|
|
96
|
+
fail("Could not verify session");
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
saveSession({
|
|
101
|
+
cookie,
|
|
102
|
+
email: me.email,
|
|
103
|
+
unlocked: me.unlocked ?? false,
|
|
104
|
+
unlockedUntil: me.unlockedUntil ?? null,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
ok(`Logged in as ${me.email}`);
|
|
108
|
+
if (me.unlocked && me.unlockedUntil) {
|
|
109
|
+
ok(`Active until ${me.unlockedUntil}`);
|
|
110
|
+
} else {
|
|
111
|
+
console.log(`\n ${YELLOW}Account not activated.${NC}\n`);
|
|
112
|
+
}
|
|
113
|
+
console.log();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function runWhoami() {
|
|
117
|
+
const session = loadSession();
|
|
118
|
+
if (!session) {
|
|
119
|
+
console.log(`\n Not logged in.\n`);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
console.log(`\n ${session.email}`);
|
|
124
|
+
if (session.unlocked && session.unlockedUntil) {
|
|
125
|
+
console.log(` Active until: ${GREEN}${session.unlockedUntil}${NC}`);
|
|
126
|
+
} else {
|
|
127
|
+
console.log(` Activated: ${YELLOW}no${NC}`);
|
|
128
|
+
}
|
|
129
|
+
console.log();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function runLogout() {
|
|
133
|
+
const { removeSkills } = await import("./skills.js");
|
|
134
|
+
removeSkills();
|
|
135
|
+
clearSession();
|
|
136
|
+
ok("Logged out");
|
|
137
|
+
console.log();
|
|
138
|
+
}
|
package/lib/new.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { cwd } from "node:process";
|
|
5
|
+
import { BASE_URL, requireSession } from "./auth.js";
|
|
6
|
+
|
|
7
|
+
const POLL_INTERVAL = 3000;
|
|
8
|
+
|
|
9
|
+
export async function runNew(description) {
|
|
10
|
+
if (!description) {
|
|
11
|
+
console.error("\n Usage: clawfirm new \"<project description>\"\n");
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const session = requireSession();
|
|
16
|
+
const headers = { "Content-Type": "application/json", Cookie: session.cookie };
|
|
17
|
+
|
|
18
|
+
// Step 1: submit job
|
|
19
|
+
let jobId;
|
|
20
|
+
try {
|
|
21
|
+
const res = await fetch(`${BASE_URL}/api/generate-whip`, {
|
|
22
|
+
method: "POST",
|
|
23
|
+
headers,
|
|
24
|
+
body: JSON.stringify({ description, save: false }),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const body = await res.json().catch(() => ({}));
|
|
28
|
+
|
|
29
|
+
if (res.status === 401) {
|
|
30
|
+
console.error("\n Session expired. Run: clawfirm login\n");
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
if (res.status === 402 || res.status === 403) {
|
|
34
|
+
console.error(`\n ${body.error ?? "Access denied. Visit https://clawfirm.dev"}\n`);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
if (!res.ok) {
|
|
38
|
+
console.error(`\n API error: ${body.error ?? res.statusText}\n`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
jobId = body.jobId;
|
|
43
|
+
} catch (e) {
|
|
44
|
+
console.error(`\n Network error: ${e.message}\n`);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Step 2: poll until done
|
|
49
|
+
const startTime = Date.now();
|
|
50
|
+
let whipContent, whipKey;
|
|
51
|
+
|
|
52
|
+
while (true) {
|
|
53
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(0);
|
|
54
|
+
process.stdout.write(`\r Generating... ${elapsed}s `);
|
|
55
|
+
|
|
56
|
+
await new Promise(r => setTimeout(r, POLL_INTERVAL));
|
|
57
|
+
|
|
58
|
+
let poll;
|
|
59
|
+
try {
|
|
60
|
+
const res = await fetch(`${BASE_URL}/api/generate-whip?job=${jobId}`, { headers });
|
|
61
|
+
poll = await res.json().catch(() => ({}));
|
|
62
|
+
|
|
63
|
+
if (res.status === 401) {
|
|
64
|
+
process.stdout.write("\r \r");
|
|
65
|
+
console.error("\n Session expired. Run: clawfirm login\n");
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
if (!res.ok) {
|
|
69
|
+
process.stdout.write("\r \r");
|
|
70
|
+
console.error(`\n API error: ${poll.error ?? res.statusText}\n`);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
} catch (e) {
|
|
74
|
+
process.stdout.write("\r \r");
|
|
75
|
+
console.error(`\n Network error: ${e.message}\n`);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (poll.status === "completed") {
|
|
80
|
+
whipKey = poll.key ?? `${jobId}/workflow-${Date.now()}.whip`;
|
|
81
|
+
if (poll.whip) {
|
|
82
|
+
whipContent = poll.whip;
|
|
83
|
+
} else {
|
|
84
|
+
// content delivered separately via /api/whips/{jobId}/{filename}
|
|
85
|
+
const dlRes = await fetch(`${BASE_URL}/api/whips/${whipKey}`, { headers });
|
|
86
|
+
whipContent = await dlRes.text();
|
|
87
|
+
if (!dlRes.ok || !whipContent) {
|
|
88
|
+
process.stdout.write("\r \r");
|
|
89
|
+
console.error(`\n Could not fetch whip content (${dlRes.status}): ${whipContent}\n`);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
if (poll.status === "error") {
|
|
96
|
+
process.stdout.write("\r \r");
|
|
97
|
+
console.error(`\n Generation failed: ${poll.error ?? "unknown error"}\n`);
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
// status === "pending" | "running" → keep polling
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
process.stdout.write("\r \r");
|
|
104
|
+
|
|
105
|
+
const whipFile = join(cwd(), whipKey.split("/").pop());
|
|
106
|
+
writeFileSync(whipFile, whipContent, "utf8");
|
|
107
|
+
console.log(` Saved: ${whipFile}\n`);
|
|
108
|
+
|
|
109
|
+
const result = spawnSync("whipflow", ["run", whipFile], { stdio: "inherit" });
|
|
110
|
+
|
|
111
|
+
if (result.error?.code === "ENOENT") {
|
|
112
|
+
console.error("\n whipflow not found. Run: clawfirm install\n");
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
process.exit(result.status ?? 0);
|
|
117
|
+
}
|
package/lib/skills.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { writeFileSync, mkdirSync, existsSync, readdirSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { BASE_URL } from "./auth.js";
|
|
5
|
+
|
|
6
|
+
const SKILLS_DIR = join(homedir(), ".skillctl", "skills");
|
|
7
|
+
|
|
8
|
+
const GREEN = "\x1b[32m";
|
|
9
|
+
const BLUE = "\x1b[34m";
|
|
10
|
+
const RED = "\x1b[31m";
|
|
11
|
+
const NC = "\x1b[0m";
|
|
12
|
+
|
|
13
|
+
const ok = (msg) => console.log(`${GREEN} ✓${NC} ${msg}`);
|
|
14
|
+
const info = (msg) => console.log(`${BLUE} →${NC} ${msg}`);
|
|
15
|
+
const fail = (msg) => console.log(`${RED} ✗${NC} ${msg}`);
|
|
16
|
+
|
|
17
|
+
export async function installSkills(session) {
|
|
18
|
+
info("Fetching skills...");
|
|
19
|
+
|
|
20
|
+
let skills;
|
|
21
|
+
try {
|
|
22
|
+
const res = await fetch(`${BASE_URL}/api/skills`, {
|
|
23
|
+
headers: { Cookie: session.cookie },
|
|
24
|
+
});
|
|
25
|
+
if (!res.ok) {
|
|
26
|
+
if (res.status !== 404) fail(`Failed to fetch skills: ${res.statusText}`);
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
skills = await res.json(); // [{ name: string, content: string }]
|
|
30
|
+
} catch (e) {
|
|
31
|
+
fail(`Network error: ${e.message}`);
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!existsSync(SKILLS_DIR)) mkdirSync(SKILLS_DIR, { recursive: true });
|
|
36
|
+
|
|
37
|
+
for (const skill of skills) {
|
|
38
|
+
writeFileSync(join(SKILLS_DIR, `${skill.name}.md`), skill.content);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
ok(`${skills.length} skill(s) installed`);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function removeSkills() {
|
|
46
|
+
if (!existsSync(SKILLS_DIR)) return;
|
|
47
|
+
const files = readdirSync(SKILLS_DIR).filter((f) => f.endsWith(".md"));
|
|
48
|
+
for (const f of files) unlinkSync(join(SKILLS_DIR, f));
|
|
49
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@harness.farm/clawfirm",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "clawfirm 7*24 weather prediction market trading engine",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"clawfirm": "./bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"bin",
|
|
14
|
+
"lib",
|
|
15
|
+
"src/core",
|
|
16
|
+
"clawfirm.config.js"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"trade": "tsx src/core/index.ts",
|
|
20
|
+
"trade:dry": "DRY_RUN=true tsx src/core/index.ts",
|
|
21
|
+
"typecheck": "tsc --noEmit --project tsconfig.core.json"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"viem": "^2.21.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^20.0.0",
|
|
31
|
+
"tsx": "^4.7.0",
|
|
32
|
+
"typescript": "^5.4.0"
|
|
33
|
+
}
|
|
34
|
+
}
|