@dreamlogic-ai/cli 1.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.
@@ -0,0 +1,204 @@
1
+ /**
2
+ * setup-mcp command — auto-configure MCP for AI agents
3
+ * D-11: Shows diff + confirmation before writing; supports --dry-run
4
+ * R1-02: execFileSync instead of execSync (no shell injection)
5
+ * R1-01: Config files written with 0o600 permissions
6
+ */
7
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from "fs";
8
+ import { join } from "path";
9
+ import { homedir, platform } from "os";
10
+ import { execFileSync } from "child_process";
11
+ import { confirm } from "@inquirer/prompts";
12
+ import { getServer, maskKey } from "../lib/config.js";
13
+ import { ui } from "../lib/ui.js";
14
+ import { requireAuth } from "./helpers.js";
15
+ import chalk from "chalk";
16
+ /** R1-01: Mask key in displayed config JSON */
17
+ function maskConfigJson(config, key) {
18
+ return JSON.stringify(config, null, 2).replace(new RegExp(key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), "g"), maskKey(key));
19
+ }
20
+ /** Write config file with restrictive permissions (R1-01) */
21
+ function writeConfigSafe(path, data) {
22
+ mkdirSync(join(path, ".."), { recursive: true });
23
+ writeFileSync(path, data, { mode: 0o600 });
24
+ try {
25
+ chmodSync(path, 0o600);
26
+ }
27
+ catch { /* Windows fallback */ }
28
+ }
29
+ function getAgents(server, key) {
30
+ const isWin = platform() === "win32";
31
+ const home = homedir();
32
+ const claudeDesktopPath = isWin
33
+ ? join(home, "AppData", "Roaming", "Claude", "claude_desktop_config.json")
34
+ : join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
35
+ return [
36
+ {
37
+ name: "Claude Desktop",
38
+ configPath: claudeDesktopPath,
39
+ detect: () => existsSync(claudeDesktopPath) || existsSync(join(claudeDesktopPath, "..")),
40
+ generate: () => ({
41
+ mcpServers: {
42
+ "dreamlogic-skills": {
43
+ url: `${server}/sse?api_key=${key}`,
44
+ },
45
+ },
46
+ }),
47
+ apply: (newSection) => {
48
+ let existing = {};
49
+ if (existsSync(claudeDesktopPath)) {
50
+ try {
51
+ existing = JSON.parse(readFileSync(claudeDesktopPath, "utf-8"));
52
+ }
53
+ catch {
54
+ // R2-07: Backup corrupted config instead of silently discarding
55
+ const bakPath = claudeDesktopPath + ".bak";
56
+ writeFileSync(bakPath, readFileSync(claudeDesktopPath));
57
+ ui.warning(`Existing config has invalid JSON — backed up to ${bakPath}`);
58
+ }
59
+ }
60
+ const merged = {
61
+ ...existing,
62
+ mcpServers: {
63
+ ...(existing.mcpServers || {}),
64
+ ...(newSection.mcpServers || {}),
65
+ },
66
+ };
67
+ writeConfigSafe(claudeDesktopPath, JSON.stringify(merged, null, 2) + "\n");
68
+ },
69
+ },
70
+ {
71
+ name: "Claude CLI",
72
+ configPath: "(uses `claude mcp add` command)",
73
+ // R1-18: Platform-specific detection
74
+ detect: () => {
75
+ try {
76
+ if (isWin) {
77
+ execFileSync("where", ["claude"], { stdio: "ignore" });
78
+ }
79
+ else {
80
+ execFileSync("sh", ["-c", "command -v claude"], { stdio: "ignore" });
81
+ }
82
+ return true;
83
+ }
84
+ catch {
85
+ return false;
86
+ }
87
+ },
88
+ generate: () => ({
89
+ // R1-02: Store args as array, not interpolated string
90
+ args: ["mcp", "add", "dreamlogic-skills", "--url", `${server}/sse?api_key=${key}`],
91
+ }),
92
+ apply: (config) => {
93
+ // R1-02: execFileSync with arg array — no shell injection possible
94
+ execFileSync("claude", config.args, { stdio: "inherit" });
95
+ },
96
+ },
97
+ {
98
+ name: "Cursor",
99
+ configPath: join(home, ".cursor", "mcp.json"),
100
+ detect: () => existsSync(join(home, ".cursor")),
101
+ generate: () => ({
102
+ mcpServers: {
103
+ "dreamlogic-skills": {
104
+ url: `${server}/sse?api_key=${key}`,
105
+ },
106
+ },
107
+ }),
108
+ apply: (newSection) => {
109
+ const path = join(home, ".cursor", "mcp.json");
110
+ let existing = {};
111
+ if (existsSync(path)) {
112
+ try {
113
+ existing = JSON.parse(readFileSync(path, "utf-8"));
114
+ }
115
+ catch {
116
+ // R2-07: Backup corrupted config
117
+ const bakPath = path + ".bak";
118
+ writeFileSync(bakPath, readFileSync(path));
119
+ ui.warning(`Existing config has invalid JSON — backed up to ${bakPath}`);
120
+ }
121
+ }
122
+ const merged = {
123
+ ...existing,
124
+ mcpServers: {
125
+ ...(existing.mcpServers || {}),
126
+ ...(newSection.mcpServers || {}),
127
+ },
128
+ };
129
+ writeConfigSafe(path, JSON.stringify(merged, null, 2) + "\n");
130
+ },
131
+ },
132
+ ];
133
+ }
134
+ export async function setupMcpCommand(opts) {
135
+ const apiKey = requireAuth();
136
+ if (!apiKey)
137
+ return;
138
+ const server = getServer();
139
+ const agents = getAgents(server, apiKey);
140
+ console.log();
141
+ ui.header("MCP Agent Configuration");
142
+ console.log();
143
+ // Detect installed agents
144
+ const detected = agents.filter((a) => a.detect());
145
+ const notDetected = agents.filter((a) => !a.detect());
146
+ if (detected.length > 0) {
147
+ ui.ok(`Detected ${detected.length} agent(s):`);
148
+ for (const a of detected)
149
+ ui.line(` ${chalk.green("●")} ${a.name}`);
150
+ }
151
+ if (notDetected.length > 0) {
152
+ ui.line(` ${ui.dim("Not detected: " + notDetected.map((a) => a.name).join(", "))}`);
153
+ }
154
+ if (detected.length === 0) {
155
+ ui.warning("No supported agents detected.");
156
+ ui.info("Supported agents: " + agents.map((a) => a.name).join(", "));
157
+ console.log();
158
+ ui.info("Manual MCP configuration:");
159
+ ui.line(` Server URL: ${server}/sse`);
160
+ ui.line(` API Key: ${maskKey(apiKey)}`);
161
+ return;
162
+ }
163
+ // R1-01: Warn about key in MCP URL (inherent to MCP SSE protocol)
164
+ ui.warning("MCP config will contain your API key in the URL (required by MCP protocol).");
165
+ ui.line(` Config files are written with restricted permissions (owner-only).`);
166
+ // Configure each detected agent
167
+ for (const agent of detected) {
168
+ console.log();
169
+ ui.line(`${chalk.bold(agent.name)}:`);
170
+ const config = agent.generate(server, apiKey);
171
+ // D-11: Show what will be written (R1-01: mask key in display)
172
+ if (agent.name === "Claude CLI") {
173
+ const maskedCmd = `claude mcp add dreamlogic-skills --url "${server}/sse?api_key=${maskKey(apiKey)}"`;
174
+ ui.line(` Command: ${chalk.cyan(maskedCmd)}`);
175
+ }
176
+ else {
177
+ ui.line(` Config: ${agent.configPath}`);
178
+ ui.line(` ${chalk.dim(maskConfigJson(config, apiKey).split("\n").join("\n "))}`);
179
+ }
180
+ if (opts.dryRun) {
181
+ ui.info("(dry-run — no changes made)");
182
+ continue;
183
+ }
184
+ const ok = await confirm({
185
+ message: `Apply configuration for ${agent.name}?`,
186
+ default: true,
187
+ });
188
+ if (!ok) {
189
+ ui.info("Skipped.");
190
+ continue;
191
+ }
192
+ try {
193
+ agent.apply(config);
194
+ ui.ok(`${agent.name} configured`);
195
+ if (agent.name === "Claude Desktop") {
196
+ ui.info("Restart Claude Desktop to apply changes.");
197
+ }
198
+ }
199
+ catch (err) {
200
+ ui.err(`Failed: ${err.message}`);
201
+ }
202
+ }
203
+ ui.goodbye();
204
+ }
@@ -0,0 +1 @@
1
+ export declare function statusCommand(): Promise<void>;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * status command — show full status overview
3
+ */
4
+ import { ApiClient } from "../lib/api-client.js";
5
+ import { getApiKey, getServer, getInstallDir, loadInstalled, loadConfig, maskKey, } from "../lib/config.js";
6
+ import { ui } from "../lib/ui.js";
7
+ import chalk from "chalk";
8
+ import { CLI_VERSION } from "../types.js";
9
+ export async function statusCommand() {
10
+ const config = loadConfig();
11
+ const installed = loadInstalled();
12
+ console.log();
13
+ ui.header("Dreamlogic CLI Status");
14
+ console.log();
15
+ // Config
16
+ ui.table([
17
+ ["CLI Version", CLI_VERSION],
18
+ ["Server", config?.server || "(not configured)"],
19
+ ["API Key", config?.api_key ? maskKey(config.api_key) : chalk.red("(not set)")],
20
+ ["Install Dir", getInstallDir()],
21
+ ]);
22
+ // Server health — R2-05: use redirect:"error" like all other fetches
23
+ const server = getServer();
24
+ try {
25
+ const res = await fetch(`${server}/health`, {
26
+ headers: { "User-Agent": "dreamlogic-cli" },
27
+ redirect: "error",
28
+ signal: AbortSignal.timeout(5000),
29
+ });
30
+ const data = (await res.json());
31
+ // R2-09: Basic shape validation
32
+ if (typeof data?.status !== "string" || typeof data?.version !== "string") {
33
+ throw new Error("Invalid health response");
34
+ }
35
+ console.log();
36
+ ui.ok(`Server: ${data.status} (v${data.version})`);
37
+ }
38
+ catch {
39
+ console.log();
40
+ ui.warning("Server unreachable");
41
+ }
42
+ // Installed skills
43
+ const entries = Object.entries(installed);
44
+ console.log();
45
+ ui.line(`📦 Installed: ${entries.length} skill(s)`);
46
+ if (entries.length > 0) {
47
+ // Check for updates if authenticated
48
+ const apiKey = getApiKey();
49
+ let remoteSkills = [];
50
+ if (apiKey) {
51
+ try {
52
+ const client = new ApiClient(server, apiKey);
53
+ remoteSkills = await client.listSkills();
54
+ }
55
+ catch { /* skip remote check */ }
56
+ }
57
+ for (const [id, info] of entries) {
58
+ const remote = remoteSkills.find((s) => s.id === id);
59
+ // R2-06: Normalize version for comparison (strip leading 'v')
60
+ const normalizeVer = (v) => v.replace(/^v/, "");
61
+ const isUpToDate = !remote?.latest_version ||
62
+ normalizeVer(remote.latest_version) === normalizeVer(info.version);
63
+ const icon = isUpToDate ? chalk.green("✓") : chalk.yellow("⬆");
64
+ const update = !isUpToDate ? chalk.yellow(` → ${remote.latest_version}`) : "";
65
+ ui.line(` ${icon} ${chalk.bold(id)} ${info.version}${update}`);
66
+ }
67
+ }
68
+ ui.goodbye();
69
+ }
@@ -0,0 +1,3 @@
1
+ export declare function updateCommand(opts: {
2
+ yes?: boolean;
3
+ }): Promise<void>;
@@ -0,0 +1,126 @@
1
+ /**
2
+ * update command — check for and apply skill updates
3
+ */
4
+ import { confirm, checkbox } from "@inquirer/prompts";
5
+ import { ApiClient } from "../lib/api-client.js";
6
+ import { getServer, loadInstalled } from "../lib/config.js";
7
+ import { installSkill } from "../lib/installer.js";
8
+ import { ui } from "../lib/ui.js";
9
+ import { requireAuth } from "./helpers.js";
10
+ import chalk from "chalk";
11
+ export async function updateCommand(opts) {
12
+ const apiKey = requireAuth();
13
+ if (!apiKey)
14
+ return;
15
+ const installed = loadInstalled();
16
+ const installedIds = Object.keys(installed);
17
+ if (installedIds.length === 0) {
18
+ ui.info("No skills installed. Run: dreamlogic install");
19
+ return;
20
+ }
21
+ const client = new ApiClient(getServer(), apiKey);
22
+ const spinner = ui.spinner("Checking for updates...");
23
+ spinner.start();
24
+ let skills;
25
+ try {
26
+ skills = await client.listSkills();
27
+ spinner.stop();
28
+ }
29
+ catch (err) {
30
+ spinner.fail(" Failed to check updates");
31
+ ui.err(err.message);
32
+ process.exitCode = 1;
33
+ return;
34
+ }
35
+ // Compare versions
36
+ const updates = [];
37
+ console.log();
38
+ for (const id of installedIds) {
39
+ const local = installed[id];
40
+ const remote = skills.find((s) => s.id === id);
41
+ if (!remote) {
42
+ ui.line(` ${ui.warn("?")} ${id} — not found on server (removed?)`);
43
+ continue;
44
+ }
45
+ if (!remote.latest_version) {
46
+ ui.line(` ${ui.dim("–")} ${remote.name} — no version info`);
47
+ continue;
48
+ }
49
+ // R1-16: Normalize version comparison (strip leading 'v')
50
+ const normalizeVer = (v) => v.replace(/^v/, "");
51
+ if (normalizeVer(local.version) === normalizeVer(remote.latest_version)) {
52
+ ui.line(` ${chalk.green("✓")} ${remote.name} ${local.version} — up to date`);
53
+ }
54
+ else {
55
+ ui.line(` ${chalk.yellow("⬆")} ${chalk.bold(remote.name)} ${local.version} → ${chalk.green(remote.latest_version)}`);
56
+ updates.push({
57
+ id: remote.id,
58
+ name: remote.name,
59
+ currentVersion: local.version,
60
+ latestVersion: remote.latest_version,
61
+ packageFile: remote.package_file || `${remote.id}-${remote.latest_version}.zip`,
62
+ sha256: remote.package_sha256,
63
+ size: remote.package_size,
64
+ });
65
+ }
66
+ }
67
+ if (updates.length === 0) {
68
+ console.log();
69
+ ui.ok("All skills are up to date! ✨");
70
+ return;
71
+ }
72
+ console.log();
73
+ ui.info(`${updates.length} update(s) available`);
74
+ // Select which to update
75
+ let selectedIds;
76
+ if (opts.yes) {
77
+ selectedIds = updates.map((u) => u.id);
78
+ }
79
+ else if (updates.length === 1) {
80
+ const ok = await confirm({
81
+ message: `Update ${updates[0].name} to ${updates[0].latestVersion}?`,
82
+ default: true,
83
+ });
84
+ selectedIds = ok ? [updates[0].id] : [];
85
+ }
86
+ else {
87
+ selectedIds = await checkbox({
88
+ message: "Select updates to apply:",
89
+ choices: updates.map((u) => ({
90
+ name: `${u.name} ${u.currentVersion} → ${u.latestVersion}${u.size ? ` (${ui.fileSize(u.size)})` : ""}`,
91
+ value: u.id,
92
+ checked: true,
93
+ })),
94
+ });
95
+ }
96
+ if (selectedIds.length === 0) {
97
+ ui.info("No updates selected.");
98
+ return;
99
+ }
100
+ // Apply updates
101
+ let successCount = 0;
102
+ for (const id of selectedIds) {
103
+ const u = updates.find((u) => u.id === id);
104
+ if (!u)
105
+ continue; // R1-19
106
+ console.log();
107
+ ui.header(`Updating ${u.name}`);
108
+ ui.line(` ${u.currentVersion} → ${chalk.green(u.latestVersion)}`);
109
+ try {
110
+ await installSkill(client, u.id, u.packageFile, u.sha256, u.latestVersion);
111
+ ui.ok(`Updated to ${u.latestVersion}`);
112
+ successCount++;
113
+ }
114
+ catch (err) {
115
+ ui.err(`Failed: ${err.message}`);
116
+ ui.info(`Rollback with: dreamlogic rollback ${u.id}`);
117
+ }
118
+ }
119
+ console.log();
120
+ if (successCount === selectedIds.length) {
121
+ ui.ok(`All ${successCount} update(s) applied! 🎉`);
122
+ }
123
+ else {
124
+ ui.warning(`${successCount}/${selectedIds.length} updated. Check errors above.`);
125
+ }
126
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Dreamlogic CLI — AI Skill Manager
4
+ * by Dreamlogic-ai · MAJORNINE
5
+ *
6
+ * Install, update and manage AI agent skills for your team.
7
+ */
8
+ import { Command } from "commander";
9
+ import { select } from "@inquirer/prompts";
10
+ import { CLI_VERSION, CLI_NAME, CLI_AUTHOR } from "./types.js";
11
+ import { ui } from "./lib/ui.js";
12
+ import { getApiKey } from "./lib/config.js";
13
+ // Commands
14
+ import { loginCommand } from "./commands/login.js";
15
+ import { logoutCommand } from "./commands/logout.js";
16
+ import { catalogCommand } from "./commands/catalog.js";
17
+ import { installCommand } from "./commands/install.js";
18
+ import { updateCommand } from "./commands/update.js";
19
+ import { listCommand } from "./commands/list.js";
20
+ import { rollbackCommand } from "./commands/rollback.js";
21
+ import { statusCommand } from "./commands/status.js";
22
+ import { setupMcpCommand } from "./commands/setup-mcp.js";
23
+ const program = new Command();
24
+ program
25
+ .name("dreamlogic")
26
+ .description(`${CLI_NAME} — AI Skill Manager\n${CLI_AUTHOR}`)
27
+ .version(CLI_VERSION, "-v, --version");
28
+ // ===== login =====
29
+ program
30
+ .command("login")
31
+ .description("Authenticate with your Dreamlogic API key")
32
+ .option("-k, --key <key>", "API key (or enter interactively)")
33
+ .action((opts) => loginCommand(opts));
34
+ // ===== logout =====
35
+ program
36
+ .command("logout")
37
+ .description("Clear saved credentials")
38
+ .action(() => logoutCommand());
39
+ // ===== catalog =====
40
+ program
41
+ .command("catalog")
42
+ .description("List available skills from server")
43
+ .action(() => catalogCommand());
44
+ // ===== install =====
45
+ program
46
+ .command("install [skills...]")
47
+ .description("Install skills (interactive if no skill specified)")
48
+ .option("-y, --yes", "Skip confirmation prompts")
49
+ .option("--from-file <path>", "Install from local ZIP file (D-10)")
50
+ .action((skills, opts) => installCommand(skills, opts));
51
+ // ===== update =====
52
+ program
53
+ .command("update")
54
+ .description("Check for and apply skill updates")
55
+ .option("-y, --yes", "Auto-apply all updates")
56
+ .action((opts) => updateCommand(opts));
57
+ // ===== list =====
58
+ program
59
+ .command("list")
60
+ .alias("ls")
61
+ .description("List locally installed skills")
62
+ .action(() => listCommand());
63
+ // ===== rollback =====
64
+ program
65
+ .command("rollback <skill>")
66
+ .description("Rollback a skill to its previous version (D-13)")
67
+ .action((skill) => rollbackCommand(skill));
68
+ // ===== status =====
69
+ program
70
+ .command("status")
71
+ .description("Show full status overview")
72
+ .action(() => statusCommand());
73
+ // ===== setup-mcp =====
74
+ program
75
+ .command("setup-mcp")
76
+ .description("Auto-configure MCP for AI agents")
77
+ .option("--dry-run", "Show what would be configured without applying")
78
+ .action((opts) => setupMcpCommand(opts));
79
+ // ===== Interactive mode (no command) =====
80
+ program.action(async () => {
81
+ ui.banner();
82
+ const isLoggedIn = !!getApiKey();
83
+ if (!isLoggedIn) {
84
+ ui.info("First time? Let's get you set up!");
85
+ console.log();
86
+ await loginCommand({});
87
+ if (!getApiKey())
88
+ return; // login failed
89
+ console.log();
90
+ }
91
+ // Interactive menu
92
+ while (true) {
93
+ console.log();
94
+ const action = await select({
95
+ message: "What would you like to do?",
96
+ choices: [
97
+ { name: "📦 Install skills", value: "install" },
98
+ { name: "🔄 Check for updates", value: "update" },
99
+ { name: "📋 List installed skills", value: "list" },
100
+ { name: "⚙️ Configure MCP for Agent", value: "setup-mcp" },
101
+ { name: "📊 Status overview", value: "status" },
102
+ { name: "📚 Browse skill catalog", value: "catalog" },
103
+ { name: "🚪 Exit", value: "exit" },
104
+ ],
105
+ });
106
+ switch (action) {
107
+ case "install":
108
+ await installCommand([], {});
109
+ break;
110
+ case "update":
111
+ await updateCommand({});
112
+ break;
113
+ case "list":
114
+ listCommand();
115
+ break;
116
+ case "setup-mcp":
117
+ await setupMcpCommand({});
118
+ break;
119
+ case "status":
120
+ await statusCommand();
121
+ break;
122
+ case "catalog":
123
+ await catalogCommand();
124
+ break;
125
+ case "exit":
126
+ ui.goodbye();
127
+ return;
128
+ }
129
+ }
130
+ });
131
+ // Handle errors gracefully
132
+ program.exitOverride();
133
+ async function main() {
134
+ try {
135
+ await program.parseAsync(process.argv);
136
+ }
137
+ catch (err) {
138
+ if (err && typeof err === "object" && "code" in err) {
139
+ const code = err.code;
140
+ // Commander built-in exit (help, version)
141
+ if (code === "commander.helpDisplayed" || code === "commander.version") {
142
+ return;
143
+ }
144
+ }
145
+ // User cancelled (Ctrl+C on inquirer)
146
+ if (err && typeof err === "object" && "name" in err && err.name === "ExitPromptError") {
147
+ console.log();
148
+ ui.dim(" Cancelled.");
149
+ return;
150
+ }
151
+ ui.err(`Unexpected error: ${err.message}`);
152
+ process.exitCode = 1;
153
+ }
154
+ }
155
+ main();
@@ -0,0 +1,25 @@
1
+ import { type SkillInfo, type UserInfo } from "../types.js";
2
+ export declare class ApiClient {
3
+ private baseUrl;
4
+ private apiKey;
5
+ constructor(baseUrl: string, apiKey: string);
6
+ private request;
7
+ /** GET /api/me — verify key + get user info */
8
+ me(): Promise<UserInfo>;
9
+ /** GET /skills — list available skills with version info */
10
+ listSkills(): Promise<SkillInfo[]>;
11
+ /** GET /packages/:filename — download package as buffer (R1-11: size limited) */
12
+ downloadPackage(filename: string, onProgress?: (downloaded: number, total: number) => void): Promise<{
13
+ buffer: Buffer;
14
+ sha256: string;
15
+ }>;
16
+ /** GET /health — check server connectivity (R2-09: validated) */
17
+ health(): Promise<{
18
+ status: string;
19
+ version: string;
20
+ }>;
21
+ }
22
+ export declare class ApiError extends Error {
23
+ status: number;
24
+ constructor(message: string, status: number);
25
+ }