clawlodge-cli 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/README.md +42 -0
- package/bin/clawlodge.mjs +8 -0
- package/lib/core.mjs +480 -0
- package/package.json +30 -0
package/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# ClawLodge CLI
|
|
2
|
+
|
|
3
|
+
Pack and publish OpenClaw config workspaces to ClawLodge.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g clawlodge-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Basic usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
clawlodge login
|
|
15
|
+
clawlodge pack
|
|
16
|
+
clawlodge publish
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## README and Name
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
clawlodge publish --name "My Workspace"
|
|
23
|
+
clawlodge publish --readme /path/to/README.md
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
If you do not pass `--name`, the CLI derives it from the workspace folder name.
|
|
27
|
+
If you do not pass `--readme`, the publish API generates the README on the server.
|
|
28
|
+
|
|
29
|
+
## Help
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
clawlodge help
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Create a PAT in `https://clawlodge.com/settings`, then run:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
clawlodge login
|
|
39
|
+
clawlodge whoami
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
If the default OpenClaw workspace is not available under `~/.openclaw`, pass an explicit path with `--workspace`.
|
package/lib/core.mjs
ADDED
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import readline from "node:readline/promises";
|
|
5
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
6
|
+
|
|
7
|
+
const ALLOWED_ROOT_FILES = new Set(["AGENTS.md", "SOUL.md", "TOOLS.md", "README.md"]);
|
|
8
|
+
const ALLOWED_PREFIXES = ["skills/", "examples/", "templates/", "prompts/", ".openclaw/"];
|
|
9
|
+
const BLOCKED_DIRS = new Set([".git", ".next", "node_modules", "dist", "build", "coverage", ".idea", ".vscode", "tmp", "temp", "logs", "data"]);
|
|
10
|
+
const BLOCKED_FILE_NAMES = [/^\.env(\..+)?$/i, /^id_(rsa|dsa|ecdsa|ed25519)(\.pub)?$/i];
|
|
11
|
+
const BLOCKED_FILE_EXTENSIONS = new Set([".pem", ".key", ".p12", ".pfx", ".db", ".sqlite", ".sqlite3", ".log"]);
|
|
12
|
+
const TEXT_EXTENSIONS = new Set([".md", ".mdx", ".txt", ".json", ".jsonc", ".yaml", ".yml", ".toml", ".ini", ".cfg", ".conf", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".rb", ".go", ".rs", ".java", ".kt", ".sh", ".zsh", ".bash", ".html", ".css", ".scss", ".sql"]);
|
|
13
|
+
const ALLOWED_LICENSES = new Set(["MIT", "Apache-2.0", "CC-BY-4.0", "BSD-3-Clause", "GPL-3.0-only"]);
|
|
14
|
+
const SEMVER_RE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
15
|
+
const MAX_FILE_BYTES = 128 * 1024;
|
|
16
|
+
const MAX_EXCERPT_CHARS = 1600;
|
|
17
|
+
const DEFAULT_ORIGIN = "https://clawlodge.com";
|
|
18
|
+
const CONFIG_PATH = path.join(os.homedir(), ".config", "clawlodge", "config.json");
|
|
19
|
+
const REDACTION_RULES = [
|
|
20
|
+
[/\bsk-[A-Za-z0-9]{20,}\b/g, "[REDACTED_OPENAI_KEY]"],
|
|
21
|
+
[/\bsk-or-v1-[A-Za-z0-9_-]{20,}\b/g, "[REDACTED_OPENROUTER_KEY]"],
|
|
22
|
+
[/\bgh[pousr]_[A-Za-z0-9]{20,}\b/g, "[REDACTED_GITHUB_TOKEN]"],
|
|
23
|
+
[/\bAIza[0-9A-Za-z\-_]{20,}\b/g, "[REDACTED_GEMINI_KEY]"],
|
|
24
|
+
[/\b(claw_pat_[A-Za-z0-9_-]{12,})\b/g, "[REDACTED_CLAW_PAT]"],
|
|
25
|
+
[/(Authorization:\s*Bearer\s+)[^\s"'`]+/gi, "$1[REDACTED_BEARER_TOKEN]"],
|
|
26
|
+
[/\b(Bearer\s+)[^\s"'`]+/g, "$1[REDACTED_BEARER_TOKEN]"],
|
|
27
|
+
[/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, "[REDACTED_EMAIL]"],
|
|
28
|
+
[/\b(?:\+?\d[\d\s().-]{7,}\d)\b/g, "[REDACTED_PHONE]"],
|
|
29
|
+
[/\b(?:10\.\d{1,3}|192\.168\.\d{1,3}|172\.(?:1[6-9]|2\d|3[0-1])\.\d{1,3})\.\d{1,3}\b/g, "[REDACTED_PRIVATE_IP]"],
|
|
30
|
+
[/https?:\/\/[A-Za-z0-9.-]*?(?:internal|corp|local)[A-Za-z0-9./:_-]*/gi, "[REDACTED_INTERNAL_URL]"],
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
function slugify(inputValue) {
|
|
34
|
+
return inputValue.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "lobster";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function titleCaseFromSlug(inputValue) {
|
|
38
|
+
return inputValue.split(/[-_/]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parseArgs(argv) {
|
|
42
|
+
const [command = "help", ...rest] = argv;
|
|
43
|
+
const options = {};
|
|
44
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
45
|
+
const current = rest[index];
|
|
46
|
+
if (!current.startsWith("--")) continue;
|
|
47
|
+
const key = current.slice(2);
|
|
48
|
+
const next = rest[index + 1];
|
|
49
|
+
if (!next || next.startsWith("--")) {
|
|
50
|
+
options[key] = "true";
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
options[key] = next;
|
|
54
|
+
index += 1;
|
|
55
|
+
}
|
|
56
|
+
return { command, options };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function printHelp() {
|
|
60
|
+
console.log(`ClawLodge CLI
|
|
61
|
+
|
|
62
|
+
Basic usage:
|
|
63
|
+
clawlodge login
|
|
64
|
+
clawlodge pack
|
|
65
|
+
clawlodge publish
|
|
66
|
+
|
|
67
|
+
Commands:
|
|
68
|
+
clawlodge login
|
|
69
|
+
Save a PAT locally after you create it in https://clawlodge.com/settings
|
|
70
|
+
|
|
71
|
+
clawlodge whoami
|
|
72
|
+
Show the user bound to the saved PAT
|
|
73
|
+
|
|
74
|
+
clawlodge logout
|
|
75
|
+
Remove the saved local PAT
|
|
76
|
+
|
|
77
|
+
clawlodge pack
|
|
78
|
+
Pack the default OpenClaw workspace into .clawlodge/workspace-publish.json
|
|
79
|
+
|
|
80
|
+
clawlodge publish
|
|
81
|
+
Pack the default OpenClaw workspace and publish to https://clawlodge.com
|
|
82
|
+
|
|
83
|
+
clawlodge help
|
|
84
|
+
Show this help text
|
|
85
|
+
|
|
86
|
+
Advanced usage:
|
|
87
|
+
clawlodge login --origin https://clawlodge.com
|
|
88
|
+
clawlodge pack --name "My Workspace"
|
|
89
|
+
clawlodge publish --readme /tmp/README.md
|
|
90
|
+
clawlodge pack --workspace ~/my-workspace --out /tmp/workspace-publish.json
|
|
91
|
+
clawlodge publish --workspace ~/my-workspace --token claw_pat_xxx
|
|
92
|
+
clawlodge publish --workspace ~/my-workspace --origin https://clawlodge.com
|
|
93
|
+
|
|
94
|
+
Environment variables:
|
|
95
|
+
CLAWLODGE_PAT
|
|
96
|
+
CLAWLODGE_ORIGIN
|
|
97
|
+
`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function resolveWorkspaceRoot(explicitWorkspace) {
|
|
101
|
+
if (explicitWorkspace?.trim()) {
|
|
102
|
+
return path.resolve(explicitWorkspace.trim());
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const openClawHome = path.join(process.env.HOME || process.env.USERPROFILE || "~", ".openclaw");
|
|
106
|
+
const preferredPath = path.join(openClawHome, "workspace");
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const stat = await fs.stat(preferredPath);
|
|
110
|
+
if (stat.isDirectory()) {
|
|
111
|
+
return preferredPath;
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
// fall through to workspace* discovery
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const entries = await fs.readdir(openClawHome, { withFileTypes: true });
|
|
119
|
+
const workspaceDirs = await Promise.all(
|
|
120
|
+
entries
|
|
121
|
+
.filter((entry) => entry.isDirectory() && /^workspace/i.test(entry.name))
|
|
122
|
+
.map(async (entry) => {
|
|
123
|
+
const fullPath = path.join(openClawHome, entry.name);
|
|
124
|
+
const stat = await fs.stat(fullPath);
|
|
125
|
+
return { fullPath, mtimeMs: stat.mtimeMs };
|
|
126
|
+
}),
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
workspaceDirs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
130
|
+
if (workspaceDirs[0]) {
|
|
131
|
+
return workspaceDirs[0].fullPath;
|
|
132
|
+
}
|
|
133
|
+
} catch {
|
|
134
|
+
// fall through to error
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
throw new Error(
|
|
138
|
+
"No default OpenClaw workspace found under ~/.openclaw. If your workspace is in another path, run clawlodge pack --workspace /path/to/workspace or clawlodge publish --workspace /path/to/workspace.",
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function normalizeRelativePath(root, absolutePath) {
|
|
143
|
+
return path.relative(root, absolutePath).split(path.sep).join("/");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function isBlockedFile(relativePath) {
|
|
147
|
+
const normalized = relativePath.replace(/^\.\/+/, "");
|
|
148
|
+
const parts = normalized.split("/").filter(Boolean);
|
|
149
|
+
if (!parts.length) return true;
|
|
150
|
+
if (parts.some((part) => BLOCKED_DIRS.has(part))) return true;
|
|
151
|
+
const basename = parts.at(-1) ?? "";
|
|
152
|
+
if (BLOCKED_FILE_NAMES.some((pattern) => pattern.test(basename))) return true;
|
|
153
|
+
return BLOCKED_FILE_EXTENSIONS.has(path.extname(basename).toLowerCase());
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function isAllowedFile(relativePath) {
|
|
157
|
+
const normalized = relativePath.replace(/^\.\/+/, "");
|
|
158
|
+
if (ALLOWED_ROOT_FILES.has(normalized)) return true;
|
|
159
|
+
return ALLOWED_PREFIXES.some((prefix) => normalized.startsWith(prefix));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function isTextFile(relativePath) {
|
|
163
|
+
return TEXT_EXTENSIONS.has(path.extname(relativePath).toLowerCase()) || relativePath.endsWith(".md");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function sanitizeContent(content) {
|
|
167
|
+
let next = content;
|
|
168
|
+
let maskedCount = 0;
|
|
169
|
+
for (const [pattern, replacement] of REDACTION_RULES) {
|
|
170
|
+
next = next.replace(pattern, () => {
|
|
171
|
+
maskedCount += 1;
|
|
172
|
+
return replacement;
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
next = next.replace(/\/Users\/[^/\s]+/g, "~").replace(/([A-Za-z]:\\Users\\)[^\\\s]+/g, "$1user");
|
|
176
|
+
return { content: next.trim(), maskedCount };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function buildExcerpt(content) {
|
|
180
|
+
const compact = content.trim();
|
|
181
|
+
return compact ? compact.slice(0, MAX_EXCERPT_CHARS) : null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function inferSkill(relativePath, content) {
|
|
185
|
+
if (!relativePath.startsWith("skills/")) return null;
|
|
186
|
+
const segments = relativePath.split("/");
|
|
187
|
+
const basename = segments.at(-1) ?? relativePath;
|
|
188
|
+
const folder = segments[1] ?? basename.replace(/\.[^.]+$/, "");
|
|
189
|
+
const heading = content.match(/^#\s+(.+)$/m)?.[1]?.trim();
|
|
190
|
+
return {
|
|
191
|
+
id: slugify(folder),
|
|
192
|
+
name: heading || titleCaseFromSlug(folder),
|
|
193
|
+
entry: relativePath,
|
|
194
|
+
path: relativePath,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function collectFiles(root, currentDir, shared, blocked, skills, stats) {
|
|
199
|
+
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
|
200
|
+
for (const entry of entries) {
|
|
201
|
+
const absolutePath = path.join(currentDir, entry.name);
|
|
202
|
+
const relativePath = normalizeRelativePath(root, absolutePath);
|
|
203
|
+
if (!relativePath) continue;
|
|
204
|
+
|
|
205
|
+
if (entry.isDirectory()) {
|
|
206
|
+
if (isBlockedFile(relativePath)) {
|
|
207
|
+
blocked.push(relativePath);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
await collectFiles(root, absolutePath, shared, blocked, skills, stats);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
stats.scanned_files += 1;
|
|
215
|
+
if (isBlockedFile(relativePath)) {
|
|
216
|
+
blocked.push(relativePath);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (!isAllowedFile(relativePath)) {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const fileStat = await fs.stat(absolutePath);
|
|
224
|
+
const record = {
|
|
225
|
+
path: relativePath,
|
|
226
|
+
size: fileStat.size,
|
|
227
|
+
kind: "binary",
|
|
228
|
+
content_excerpt: null,
|
|
229
|
+
content_text: null,
|
|
230
|
+
masked_count: 0,
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
if (!isTextFile(relativePath) || fileStat.size > MAX_FILE_BYTES) {
|
|
234
|
+
shared.push(record);
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const raw = await fs.readFile(absolutePath, "utf8");
|
|
239
|
+
const sanitized = sanitizeContent(raw);
|
|
240
|
+
record.kind = "text";
|
|
241
|
+
record.content_excerpt = buildExcerpt(sanitized.content);
|
|
242
|
+
record.content_text = sanitized.content || null;
|
|
243
|
+
record.masked_count = sanitized.maskedCount;
|
|
244
|
+
shared.push(record);
|
|
245
|
+
stats.masked_secrets_count += sanitized.maskedCount;
|
|
246
|
+
|
|
247
|
+
const skill = inferSkill(relativePath, sanitized.content);
|
|
248
|
+
if (skill && !skills.has(skill.id)) {
|
|
249
|
+
skills.set(skill.id, skill);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function readExplicitReadme(readmePath) {
|
|
255
|
+
const resolvedPath = path.resolve(readmePath.trim());
|
|
256
|
+
const content = await fs.readFile(resolvedPath, "utf8");
|
|
257
|
+
return content.trim();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function deriveSummary(name, summary, readme, sharedCount) {
|
|
261
|
+
if (summary?.trim()) return summary.trim();
|
|
262
|
+
const normalized = readme.replace(/^#+\s*/gm, "").replace(/`/g, "").replace(/\[(.*?)\]\((.*?)\)/g, "$1").replace(/\s+/g, " ").trim();
|
|
263
|
+
return normalized ? normalized.slice(0, 180) : `${name} workspace publish with ${sharedCount} shared files.`;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function buildPayload(options) {
|
|
267
|
+
const workspaceRoot = await resolveWorkspaceRoot(options.workspace);
|
|
268
|
+
const name = options.name?.trim() || titleCaseFromSlug(path.basename(workspaceRoot));
|
|
269
|
+
const version = options.version?.trim() || "0.1.0";
|
|
270
|
+
const license = options.license?.trim() || "MIT";
|
|
271
|
+
if (!ALLOWED_LICENSES.has(license)) throw new Error(`Unsupported license: ${license}`);
|
|
272
|
+
if (!SEMVER_RE.test(version)) throw new Error(`Invalid version: ${version}`);
|
|
273
|
+
|
|
274
|
+
const shared = [];
|
|
275
|
+
const blocked = [];
|
|
276
|
+
const skills = new Map();
|
|
277
|
+
const stats = { scanned_files: 0, shared_files: 0, blocked_files_count: 0, masked_secrets_count: 0 };
|
|
278
|
+
|
|
279
|
+
await collectFiles(workspaceRoot, workspaceRoot, shared, blocked, skills, stats);
|
|
280
|
+
shared.sort((a, b) => a.path.localeCompare(b.path));
|
|
281
|
+
blocked.sort((a, b) => a.localeCompare(b));
|
|
282
|
+
|
|
283
|
+
stats.shared_files = shared.length;
|
|
284
|
+
stats.blocked_files_count = blocked.length;
|
|
285
|
+
|
|
286
|
+
const tags = [...new Set(String(options.tags ?? "").split(",").map((item) => item.trim().toLowerCase()).filter(Boolean))];
|
|
287
|
+
const explicitReadmePath = options.readme?.trim();
|
|
288
|
+
|
|
289
|
+
let readme = "";
|
|
290
|
+
if (explicitReadmePath) {
|
|
291
|
+
readme = await readExplicitReadme(explicitReadmePath);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
workspaceRoot,
|
|
296
|
+
payload: {
|
|
297
|
+
lobster_slug: options.slug?.trim() || slugify(name),
|
|
298
|
+
name,
|
|
299
|
+
summary: deriveSummary(name, options.summary, readme, shared.length),
|
|
300
|
+
license,
|
|
301
|
+
version,
|
|
302
|
+
changelog: options.changelog?.trim() || "Initial workspace publish",
|
|
303
|
+
tags,
|
|
304
|
+
readme_markdown: readme || undefined,
|
|
305
|
+
source_repo: options.source_repo?.trim() || undefined,
|
|
306
|
+
source_commit: options.source_commit?.trim() || undefined,
|
|
307
|
+
publish_client: "clawlodge-cli/0.1.0",
|
|
308
|
+
workspace_files: shared,
|
|
309
|
+
blocked_files: blocked,
|
|
310
|
+
skills: Array.from(skills.values()),
|
|
311
|
+
settings: [
|
|
312
|
+
{ key: "tags", value: tags },
|
|
313
|
+
{ key: "blocked_files", value: blocked },
|
|
314
|
+
{ key: "workspace_stats", value: stats },
|
|
315
|
+
],
|
|
316
|
+
stats,
|
|
317
|
+
},
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async function writePack(outputPath, payload) {
|
|
322
|
+
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
323
|
+
await fs.writeFile(outputPath, JSON.stringify(payload, null, 2), "utf8");
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function readConfig() {
|
|
327
|
+
try {
|
|
328
|
+
const raw = await fs.readFile(CONFIG_PATH, "utf8");
|
|
329
|
+
return JSON.parse(raw);
|
|
330
|
+
} catch {
|
|
331
|
+
return {};
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async function writeConfig(config) {
|
|
336
|
+
await fs.mkdir(path.dirname(CONFIG_PATH), { recursive: true, mode: 0o700 });
|
|
337
|
+
await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2), { encoding: "utf8", mode: 0o600 });
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function clearConfig() {
|
|
341
|
+
await fs.rm(CONFIG_PATH, { force: true });
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function resolveOrigin(options, config = {}) {
|
|
345
|
+
return options.origin?.trim() || process.env.CLAWLODGE_ORIGIN || config.origin?.trim() || DEFAULT_ORIGIN;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function resolveToken(options, config = {}) {
|
|
349
|
+
return options.token?.trim() || process.env.CLAWLODGE_PAT || config.token?.trim() || "";
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async function promptForToken(origin) {
|
|
353
|
+
if (!input.isTTY || !output.isTTY) {
|
|
354
|
+
throw new Error(`Missing PAT. Create one at ${origin}/settings, then run clawlodge login --token claw_pat_xxx or set CLAWLODGE_PAT.`);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
console.log(`Create a PAT at ${origin}/settings and paste it below.`);
|
|
358
|
+
const rl = readline.createInterface({ input, output });
|
|
359
|
+
try {
|
|
360
|
+
return (await rl.question("PAT: ")).trim();
|
|
361
|
+
} finally {
|
|
362
|
+
rl.close();
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function requestJson(url, token, init = {}) {
|
|
367
|
+
const response = await fetch(url, {
|
|
368
|
+
...init,
|
|
369
|
+
headers: {
|
|
370
|
+
"Content-Type": "application/json",
|
|
371
|
+
Authorization: `Bearer ${token}`,
|
|
372
|
+
...(init.headers ?? {}),
|
|
373
|
+
},
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
const body = await response.json().catch(() => ({}));
|
|
377
|
+
if (!response.ok) {
|
|
378
|
+
throw new Error(body?.detail || `Request failed: ${response.status}`);
|
|
379
|
+
}
|
|
380
|
+
return body;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async function fetchPatProfile(origin, token) {
|
|
384
|
+
return requestJson(`${origin.replace(/\/$/, "")}/api/v1/me/pat`, token, { method: "GET" });
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async function runLogin(options) {
|
|
388
|
+
const config = await readConfig();
|
|
389
|
+
const origin = resolveOrigin(options, config);
|
|
390
|
+
const token = options.token?.trim() || process.env.CLAWLODGE_PAT || await promptForToken(origin);
|
|
391
|
+
if (!token) {
|
|
392
|
+
throw new Error("PAT required");
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const profile = await fetchPatProfile(origin, token);
|
|
396
|
+
await writeConfig({ origin, token });
|
|
397
|
+
console.log(JSON.stringify({
|
|
398
|
+
ok: true,
|
|
399
|
+
mode: "login",
|
|
400
|
+
origin,
|
|
401
|
+
user: profile.user,
|
|
402
|
+
active_token_prefix: profile.active_token_prefix,
|
|
403
|
+
config_path: CONFIG_PATH,
|
|
404
|
+
}, null, 2));
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async function runWhoAmI(options) {
|
|
408
|
+
const config = await readConfig();
|
|
409
|
+
const origin = resolveOrigin(options, config);
|
|
410
|
+
const token = resolveToken(options, config);
|
|
411
|
+
if (!token) {
|
|
412
|
+
throw new Error(`Missing PAT. Create one at ${origin}/settings, then run clawlodge login.`);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const profile = await fetchPatProfile(origin, token);
|
|
416
|
+
console.log(JSON.stringify({
|
|
417
|
+
ok: true,
|
|
418
|
+
mode: "whoami",
|
|
419
|
+
origin,
|
|
420
|
+
user: profile.user,
|
|
421
|
+
active_token_prefix: profile.active_token_prefix,
|
|
422
|
+
active_token_last_used_at: profile.active_token_last_used_at,
|
|
423
|
+
}, null, 2));
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async function runLogout() {
|
|
427
|
+
await clearConfig();
|
|
428
|
+
console.log(JSON.stringify({ ok: true, mode: "logout", config_path: CONFIG_PATH }, null, 2));
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export async function runCli(argv = process.argv.slice(2)) {
|
|
432
|
+
const { command, options } = parseArgs(argv);
|
|
433
|
+
if (command === "help" || command === "--help" || command === "-h") {
|
|
434
|
+
printHelp();
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (command === "login") {
|
|
439
|
+
await runLogin(options);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (command === "whoami") {
|
|
444
|
+
await runWhoAmI(options);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (command === "logout") {
|
|
449
|
+
await runLogout();
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const { workspaceRoot, payload } = await buildPayload(options);
|
|
454
|
+
const out = path.resolve(options.out ?? path.join(workspaceRoot, ".clawlodge", "workspace-publish.json"));
|
|
455
|
+
|
|
456
|
+
if (command === "pack") {
|
|
457
|
+
await writePack(out, payload);
|
|
458
|
+
console.log(JSON.stringify({ ok: true, mode: "pack", out, stats: payload.stats }, null, 2));
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (command !== "publish") {
|
|
463
|
+
throw new Error(`Unsupported command: ${command}`);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const config = await readConfig();
|
|
467
|
+
const origin = resolveOrigin(options, config);
|
|
468
|
+
const token = resolveToken(options, config);
|
|
469
|
+
if (!token) {
|
|
470
|
+
throw new Error(`Missing PAT. Create one at ${origin}/settings, then run clawlodge login, pass --token, or set CLAWLODGE_PAT.`);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
await writePack(out, payload);
|
|
474
|
+
const body = await requestJson(`${origin.replace(/\/$/, "")}/api/v1/workspace/publish`, token, {
|
|
475
|
+
method: "POST",
|
|
476
|
+
body: JSON.stringify(payload),
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
console.log(JSON.stringify({ ok: true, mode: "publish", out, origin, result: body }, null, 2));
|
|
480
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "clawlodge-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI for packing and publishing OpenClaw configs to ClawLodge",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+ssh://git@github.com/memepilot/clawlodge.git",
|
|
8
|
+
"directory": "packages/clawlodge-cli"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://clawlodge.com/publish",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/memepilot/clawlodge/issues"
|
|
13
|
+
},
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"bin": {
|
|
18
|
+
"clawlodge": "bin/clawlodge.mjs"
|
|
19
|
+
},
|
|
20
|
+
"type": "module",
|
|
21
|
+
"files": [
|
|
22
|
+
"bin/clawlodge.mjs",
|
|
23
|
+
"lib/core.mjs",
|
|
24
|
+
"README.md"
|
|
25
|
+
],
|
|
26
|
+
"license": "UNLICENSED",
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=18"
|
|
29
|
+
}
|
|
30
|
+
}
|