playnex-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 ADDED
@@ -0,0 +1,15 @@
1
+ # Playnex CLI
2
+
3
+ Local-first CLI for managing Playnex agents, capsules, and tools.
4
+
5
+ ## Install dependencies
6
+
7
+ npm install
8
+
9
+ ## Link globally (for development)
10
+
11
+ npm link
12
+
13
+ ## Commands
14
+
15
+ playnex login
package/bin/playnex.js ADDED
@@ -0,0 +1,229 @@
1
+ #!/usr/bin/env node
2
+ import { program } from "commander";
3
+ import chalk from "chalk";
4
+ import login from "../src/commands/login.js";
5
+
6
+ // -----------------------------------------------------
7
+ // Custom Help Banner
8
+ // -----------------------------------------------------
9
+ program.configureHelp({
10
+ sortSubcommands: false,
11
+ sortOptions: false,
12
+ formatHelp: (cmd, helper) => {
13
+ const b = chalk.bold;
14
+ const g = chalk.gray;
15
+
16
+ return `
17
+ ${b("Playnex CLI — Local‑First AI Agents")}
18
+ ${g("────────────────────────────────────────────")}
19
+
20
+ Playnex is a local‑first automation platform.
21
+ Agents run on your machine, use local tools, and sync with your Playnex workspace.
22
+
23
+ ${b("Quick Start")}
24
+ playnex login
25
+ playnex agent create
26
+ playnex agent chat myagent
27
+
28
+ ${b("Core Concepts")}
29
+ Agents Autonomous workers you define (model, purpose, tools)
30
+ Workspace Your synced project files and context
31
+ Capsules Versioned knowledge bundles mirrored locally
32
+ Scheduler Background automation for agents
33
+ Tools Capabilities you attach to agents
34
+
35
+ ${b("Agent Commands")}
36
+ agent create Create a new agent
37
+ agent chat <name> Chat with an agent
38
+ agent run <name> Run an agent in automation mode
39
+ agent add-tool <n> <t> Attach a tool to an agent
40
+
41
+ ${b("Workspace")}
42
+ workspace sync Pull workspace from cloud
43
+
44
+ ${b("Capsules")}
45
+ capsules mirror Mirror all capsules locally
46
+
47
+ ${b("Scheduler")}
48
+ scheduler add <a> <int> Schedule an agent (e.g. 1h, 30m)
49
+ scheduler list List scheduled agents
50
+ scheduler remove <id> Remove a scheduled agent
51
+ scheduler run Run scheduler daemon
52
+
53
+ ${b("Tools")}
54
+ tools register <tool> Register a tool
55
+ tools list List tools
56
+ tools info <tool> Show tool details
57
+
58
+ ${b("Examples")}
59
+ playnex agent create
60
+ playnex agent chat writer
61
+ playnex scheduler add reporter 1h
62
+ playnex tools register websearch
63
+
64
+ ${b("Options")}
65
+ -h, --help Show help
66
+ -V, --version Show version
67
+
68
+ ${g("Tip: Run 'playnex <command> --help' for detailed usage.")}
69
+ `;
70
+ }
71
+ });
72
+
73
+
74
+ // -----------------------------------------------------
75
+ // Base CLI metadata
76
+ // -----------------------------------------------------
77
+ program
78
+ .name("playnex")
79
+ .description("Playnex CLI — local-first AI agents")
80
+ .version("0.1.0");
81
+
82
+ // -----------------------------------------------------
83
+ // Authentication
84
+ // -----------------------------------------------------
85
+ program
86
+ .command("login")
87
+ .description("Log in to your Playnex account")
88
+ .action(login);
89
+
90
+ // -----------------------------------------------------
91
+ // Agent Commands
92
+ // -----------------------------------------------------
93
+ const agent = program.command("agent").description("Manage Playnex agents");
94
+
95
+ agent
96
+ .command("create")
97
+ .description("Create a new Playnex agent")
98
+ .action(() =>
99
+ import("../src/commands/agent/create.js").then((m) => m.default())
100
+ );
101
+
102
+ agent
103
+ .command("chat <name>")
104
+ .description("Chat with a Playnex agent")
105
+ .action((name) =>
106
+ import("../src/commands/agent/chat.js").then((m) => m.default(name))
107
+ );
108
+
109
+ agent
110
+ .command("run <name>")
111
+ .description("Run a Playnex agent in automation mode")
112
+ .action((name) =>
113
+ import("../src/commands/agent/run.js").then((m) => m.default(name))
114
+ );
115
+
116
+ agent
117
+ .command("add-tool <name> <tool>")
118
+ .description("Add a tool to a Playnex agent")
119
+ .action((name, tool) =>
120
+ import("../src/commands/agent/add-tool.js").then((m) =>
121
+ m.default(name, tool)
122
+ )
123
+ );
124
+
125
+ // -----------------------------------------------------
126
+ // Workspace Commands
127
+ // -----------------------------------------------------
128
+ const workspace = program
129
+ .command("workspace")
130
+ .description("Workspace operations");
131
+
132
+ workspace
133
+ .command("sync")
134
+ .description("Sync Playnex workspace from cloud to local")
135
+ .action(() =>
136
+ import("../src/commands/workspace/sync.js").then((m) => m.default())
137
+ );
138
+
139
+ // -----------------------------------------------------
140
+ // Capsule Commands
141
+ // -----------------------------------------------------
142
+ const capsules = program
143
+ .command("capsules")
144
+ .description("Capsule operations");
145
+
146
+ capsules
147
+ .command("mirror")
148
+ .description("Mirror all capsules locally for offline use")
149
+ .action(() =>
150
+ import("../src/commands/capsules/mirror.js").then((m) => m.default())
151
+ );
152
+
153
+ // -----------------------------------------------------
154
+ // Scheduler Commands
155
+ // -----------------------------------------------------
156
+ const scheduler = program
157
+ .command("scheduler")
158
+ .description("Agent scheduler");
159
+
160
+ scheduler
161
+ .command("add <agent> <interval>")
162
+ .description("Schedule an agent to run automatically")
163
+ .action((agentName, interval) =>
164
+ import("../src/commands/scheduler/scheduler.js").then((m) =>
165
+ m.schedulerAdd(agentName, interval)
166
+ )
167
+ );
168
+
169
+ scheduler
170
+ .command("list")
171
+ .description("List scheduled agents")
172
+ .action(() =>
173
+ import("../src/commands/scheduler/scheduler.js").then((m) =>
174
+ m.schedulerList()
175
+ )
176
+ );
177
+
178
+ scheduler
179
+ .command("remove <id>")
180
+ .description("Remove a scheduled agent")
181
+ .action((id) =>
182
+ import("../src/commands/scheduler/scheduler.js").then((m) =>
183
+ m.schedulerRemove(id)
184
+ )
185
+ );
186
+
187
+ scheduler
188
+ .command("run")
189
+ .description("Run the scheduler daemon")
190
+ .action(() =>
191
+ import("../src/commands/scheduler/scheduler.js").then((m) =>
192
+ m.schedulerRun()
193
+ )
194
+ );
195
+
196
+ // -----------------------------------------------------
197
+ // Tool Registry Commands
198
+ // -----------------------------------------------------
199
+ const tools = program.command("tools").description("Tool registry operations");
200
+
201
+ tools
202
+ .command("register <tool>")
203
+ .description("Register a tool in the tool registry")
204
+ .action((tool) =>
205
+ import("../src/commands/tools/registry.js").then((m) =>
206
+ m.registerTool(tool)
207
+ )
208
+ );
209
+
210
+ tools
211
+ .command("list")
212
+ .description("List all registered tools")
213
+ .action(() =>
214
+ import("../src/commands/tools/registry.js").then((m) => m.listTools())
215
+ );
216
+
217
+ tools
218
+ .command("info <tool>")
219
+ .description("Show detailed info for a tool")
220
+ .action((tool) =>
221
+ import("../src/commands/tools/registry.js").then((m) =>
222
+ m.toolInfo(tool)
223
+ )
224
+ );
225
+
226
+ // -----------------------------------------------------
227
+ // Parse CLI
228
+ // -----------------------------------------------------
229
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "playnex-cli",
3
+ "version": "0.1.0",
4
+ "description": "Playnex CLI — local-first AI agents, capsules, and tools.",
5
+ "bin": {
6
+ "playnex": "./bin/playnex.js"
7
+ },
8
+ "type": "module",
9
+ "engines": {
10
+ "node": ">=18"
11
+ },
12
+ "dependencies": {
13
+ "axios": "^1.6.0",
14
+ "chalk": "^5.3.0",
15
+ "commander": "^12.0.0",
16
+ "inquirer": "^13.2.5",
17
+ "ollama": "^0.6.3"
18
+ },
19
+ "license": "MIT"
20
+ }
@@ -0,0 +1,30 @@
1
+ $ErrorActionPreference = 'Stop'
2
+
3
+ function Ensure-Dir($path) {
4
+ if (-not (Test-Path $path)) {
5
+ New-Item -ItemType Directory -Path $path | Out-Null
6
+ }
7
+ }
8
+
9
+ Write-Host 'Creating agent system...'
10
+
11
+ $root = Get-Location
12
+ $src = Join-Path $root 'src'
13
+ $services = Join-Path $src 'services'
14
+ $commands = Join-Path $src 'commands'
15
+ $agentCmd = Join-Path $commands 'agent'
16
+
17
+ Ensure-Dir $src
18
+ Ensure-Dir $services
19
+ Ensure-Dir $commands
20
+ Ensure-Dir $agentCmd
21
+
22
+ $agentsJs = @'
23
+ import fs from "fs";
24
+ export const test = true;
25
+ '@
26
+
27
+ Set-Content -Path (Join-Path $services 'agents.js') -Value $agentsJs -Encoding UTF8
28
+
29
+ Write-Host 'Done.'
30
+ Write-Host 'Run: npm install commander chalk inquirer ollama'
@@ -0,0 +1,40 @@
1
+ import fs from "fs";
2
+ import os from "os";
3
+ import path from "path";
4
+ import chalk from "chalk";
5
+
6
+ const AGENTS_DIR = path.join(os.homedir(), ".playnex", "workspace", "agents");
7
+ const TOOLS_DIR = path.join(process.cwd(), "src", "tools");
8
+
9
+ function loadAgent(name) {
10
+ const file = path.join(AGENTS_DIR, `${name}.json`);
11
+ if (!fs.existsSync(file)) {
12
+ console.log(chalk.red(`Agent '${name}' not found.`));
13
+ process.exit(1);
14
+ }
15
+ return { file, data: JSON.parse(fs.readFileSync(file, "utf8")) };
16
+ }
17
+
18
+ export default async function addTool(name, toolName) {
19
+ const { file, data } = loadAgent(name);
20
+
21
+ console.log(chalk.cyan(`\nAdd a tool to agent: ${name}\n`));
22
+
23
+ const availableTools = fs
24
+ .readdirSync(TOOLS_DIR)
25
+ .filter((f) => f.endsWith(".js"))
26
+ .map((f) => f.replace(".js", ""));
27
+
28
+ if (!availableTools.includes(toolName)) {
29
+ console.log(chalk.red(`Tool '${toolName}' does not exist.`));
30
+ process.exit(1);
31
+ }
32
+
33
+ if (!data.tools.includes(toolName)) {
34
+ data.tools.push(toolName);
35
+ }
36
+
37
+ fs.writeFileSync(file, JSON.stringify(data, null, 2), "utf8");
38
+
39
+ console.log(chalk.green(`\n✔ Tool '${toolName}' added to agent '${name}'.\n`));
40
+ }
@@ -0,0 +1,90 @@
1
+ import fs from "fs";
2
+ import os from "os";
3
+ import path from "path";
4
+ import chalk from "chalk";
5
+ import readline from "readline";
6
+ import ollama from "ollama";
7
+
8
+ const AGENTS_DIR = path.join(os.homedir(), ".playnex", "workspace", "agents");
9
+
10
+ function loadAgent(name) {
11
+ const file = path.join(AGENTS_DIR, `${name}.json`);
12
+ if (!fs.existsSync(file)) {
13
+ console.log(chalk.red(`Agent '${name}' not found.`));
14
+ process.exit(1);
15
+ }
16
+ return JSON.parse(fs.readFileSync(file, "utf8"));
17
+ }
18
+
19
+ function askUser(promptText) {
20
+ const rl = readline.createInterface({
21
+ input: process.stdin,
22
+ output: process.stdout,
23
+ });
24
+
25
+ return new Promise((resolve) =>
26
+ rl.question(promptText, (answer) => {
27
+ rl.close();
28
+ resolve(answer.trim());
29
+ })
30
+ );
31
+ }
32
+
33
+ export default async function agentChat(name) {
34
+ const agent = loadAgent(name);
35
+
36
+ // ⭐ NEW: Validate model before doing anything else
37
+ if (!agent.model || typeof agent.model !== "string") {
38
+ console.log(chalk.red(`\nAgent '${name}' has no model assigned.`));
39
+ console.log(chalk.yellow("Edit the agent JSON and set a model, e.g.:"));
40
+ console.log(chalk.gray(` "model": "llama3"`));
41
+ process.exit(1);
42
+ }
43
+
44
+ console.log(chalk.cyan(`\nChatting with agent: ${name}`));
45
+ console.log(chalk.gray(`Model: ${agent.model}`));
46
+ console.log(chalk.gray(`Purpose: ${agent.purpose}\n`));
47
+
48
+ let messages = [
49
+ {
50
+ role: "system",
51
+ content: agent.purpose,
52
+ },
53
+ ];
54
+
55
+ while (true) {
56
+ const userInput = await askUser(chalk.green("You: "));
57
+
58
+ if (!userInput) continue;
59
+ if (userInput.toLowerCase() === "exit") {
60
+ console.log(chalk.yellow("\nExiting chat.\n"));
61
+ process.exit(0);
62
+ }
63
+
64
+ messages.push({ role: "user", content: userInput });
65
+
66
+ console.log(chalk.cyan("\nAgent: "));
67
+
68
+ let responseText = "";
69
+
70
+ // ⭐ This now always has a valid model
71
+ const stream = await ollama.chat({
72
+ model: agent.model,
73
+ messages,
74
+ stream: true,
75
+ });
76
+
77
+ for await (const chunk of stream) {
78
+ const token = chunk.message?.content || "";
79
+ responseText += token;
80
+ process.stdout.write(token);
81
+ }
82
+
83
+ console.log("\n");
84
+
85
+ messages.push({
86
+ role: "assistant",
87
+ content: responseText,
88
+ });
89
+ }
90
+ }
@@ -0,0 +1,62 @@
1
+ import fs from "fs";
2
+ import os from "os";
3
+ import path from "path";
4
+ import chalk from "chalk";
5
+ import readline from "readline";
6
+ import { loadConfig } from "../../services/config.js";
7
+
8
+ const AGENTS_DIR = path.join(os.homedir(), ".playnex", "workspace", "agents");
9
+
10
+ function ask(question) {
11
+ const rl = readline.createInterface({
12
+ input: process.stdin,
13
+ output: process.stdout,
14
+ });
15
+
16
+ return new Promise((resolve) =>
17
+ rl.question(question, (answer) => {
18
+ rl.close();
19
+ resolve(answer.trim());
20
+ })
21
+ );
22
+ }
23
+
24
+ export default async function agentCreate() {
25
+ console.log(chalk.cyan("\nCreate a New Playnex Agent\n"));
26
+
27
+ const config = loadConfig();
28
+ if (!config.token) {
29
+ console.log(chalk.red("You must log in first."));
30
+ console.log("Run: playnex login");
31
+ process.exit(1);
32
+ }
33
+
34
+ const name = await ask("Agent name: ");
35
+ const purpose = await ask("Purpose (one sentence): ");
36
+
37
+ let model = "";
38
+ while (!model) {
39
+ model = await ask("Model (e.g., llama3): ");
40
+ if (!model) {
41
+ console.log(chalk.yellow("Model is required. Please enter a model name.\n"));
42
+ }
43
+ }
44
+
45
+ const agent = {
46
+ name,
47
+ purpose,
48
+ model,
49
+ tools: [],
50
+ createdAt: new Date().toISOString(),
51
+ };
52
+
53
+ if (!fs.existsSync(AGENTS_DIR)) {
54
+ fs.mkdirSync(AGENTS_DIR, { recursive: true });
55
+ }
56
+
57
+ const filePath = path.join(AGENTS_DIR, `${name}.json`);
58
+ fs.writeFileSync(filePath, JSON.stringify(agent, null, 2), "utf8");
59
+
60
+ console.log(chalk.green(`\n✔ Agent '${name}' created successfully.`));
61
+ console.log(chalk.gray(`Saved to: ${filePath}\n`));
62
+ }
@@ -0,0 +1,102 @@
1
+ import fs from "fs";
2
+ import os from "os";
3
+ import path from "path";
4
+ import chalk from "chalk";
5
+ import ollama from "ollama";
6
+
7
+ const AGENTS_DIR = path.join(os.homedir(), ".playnex", "workspace", "agents");
8
+ const TOOLS_DIR = path.join(process.cwd(), "src", "tools");
9
+
10
+ function loadAgent(name) {
11
+ const file = path.join(AGENTS_DIR, `${name}.json`);
12
+ if (!fs.existsSync(file)) {
13
+ console.log(chalk.red(`Agent '${name}' not found.`));
14
+ process.exit(1);
15
+ }
16
+ return JSON.parse(fs.readFileSync(file, "utf8"));
17
+ }
18
+
19
+ async function loadTool(toolName) {
20
+ const toolPath = path.join(TOOLS_DIR, `${toolName}.js`);
21
+ if (!fs.existsSync(toolPath)) {
22
+ console.log(chalk.red(`Tool '${toolName}' not found in src/tools.`));
23
+ process.exit(1);
24
+ }
25
+ const module = await import(toolPath);
26
+ return module.default;
27
+ }
28
+
29
+ export default async function agentRun(name) {
30
+ const agent = loadAgent(name);
31
+
32
+ console.log(chalk.cyan(`\nRunning agent: ${name}`));
33
+ console.log(chalk.gray(`Model: ${agent.model}`));
34
+ console.log(chalk.gray(`Tools: ${agent.tools.join(", ")}`));
35
+ console.log(chalk.gray(`Purpose: ${agent.purpose}\n`));
36
+
37
+ const systemPrompt = `
38
+ You are the agent: ${name}
39
+ Purpose: ${agent.purpose}
40
+
41
+ You are running in automation mode.
42
+ Think step-by-step.
43
+ If you want to use a tool, output JSON ONLY in this format:
44
+
45
+ {
46
+ "tool": "tool-name",
47
+ "input": "text to send to the tool"
48
+ }
49
+
50
+ Otherwise, output your final result as plain text.
51
+ `;
52
+
53
+ const messages = [
54
+ { role: "system", content: systemPrompt },
55
+ { role: "user", content: "Begin your autonomous reasoning cycle." },
56
+ ];
57
+
58
+ console.log(chalk.cyan("Agent output:\n"));
59
+
60
+ let responseText = "";
61
+
62
+ const stream = await ollama.chat({
63
+ model: agent.model,
64
+ messages,
65
+ stream: true,
66
+ });
67
+
68
+ for await (const chunk of stream) {
69
+ const token = chunk.message?.content || "";
70
+ responseText += token;
71
+ process.stdout.write(token);
72
+ }
73
+
74
+ console.log("\n");
75
+
76
+ // Try to detect tool JSON
77
+ let toolCall = null;
78
+ try {
79
+ const start = responseText.indexOf("{");
80
+ const end = responseText.lastIndexOf("}");
81
+ if (start !== -1 && end !== -1) {
82
+ const json = responseText.slice(start, end + 1);
83
+ toolCall = JSON.parse(json);
84
+ }
85
+ } catch {}
86
+
87
+ if (toolCall && toolCall.tool) {
88
+ console.log(chalk.yellow(`\n🔧 Agent requested tool: ${toolCall.tool}`));
89
+
90
+ if (!agent.tools.includes(toolCall.tool)) {
91
+ console.log(chalk.red(`Agent does not have access to tool '${toolCall.tool}'.`));
92
+ process.exit(1);
93
+ }
94
+
95
+ const toolFn = await loadTool(toolCall.tool);
96
+ await toolFn(agent, toolCall.input);
97
+
98
+ console.log(chalk.green("\n✔ Tool execution complete.\n"));
99
+ } else {
100
+ console.log(chalk.green("\n✔ Automation cycle complete.\n"));
101
+ }
102
+ }
@@ -0,0 +1,58 @@
1
+ import fs from "fs";
2
+ import os from "os";
3
+ import path from "path";
4
+ import chalk from "chalk";
5
+ import axios from "axios";
6
+ import { loadConfig } from "../../services/config.js";
7
+
8
+ /**
9
+ * capsules mirror
10
+ * Creates a full local mirror of all capsules for offline use.
11
+ */
12
+
13
+ const API_BASE = "https://playnex.app";
14
+
15
+ export default async function capsuleMirror() {
16
+ const config = loadConfig();
17
+
18
+ if (!config.token) {
19
+ console.log(chalk.red("You must log in first."));
20
+ console.log("Run: playnex login");
21
+ process.exit(1);
22
+ }
23
+
24
+ console.log(chalk.cyan("\nMirroring capsules to local workspace..."));
25
+
26
+ const WORKSPACE_DIR = path.join(os.homedir(), ".playnex", "workspace");
27
+ const CAPSULES_DIR = path.join(WORKSPACE_DIR, "capsules-local");
28
+
29
+ // Ensure directory exists
30
+ if (!fs.existsSync(CAPSULES_DIR)) {
31
+ fs.mkdirSync(CAPSULES_DIR, { recursive: true });
32
+ }
33
+
34
+ console.log(chalk.gray("Fetching capsules from cloud..."));
35
+
36
+ const res = await axios.get(
37
+ `${API_BASE}/capsules/all`,
38
+ {
39
+ headers: { Authorization: `Bearer ${config.token}` }
40
+ }
41
+ );
42
+
43
+ const capsules = res.data || [];
44
+
45
+ if (!Array.isArray(capsules) || capsules.length === 0) {
46
+ console.log(chalk.yellow("No capsules found."));
47
+ return;
48
+ }
49
+
50
+ // Write each capsule locally
51
+ capsules.forEach(capsule => {
52
+ const file = path.join(CAPSULES_DIR, `${capsule.id}.json`);
53
+ fs.writeFileSync(file, JSON.stringify(capsule, null, 2), "utf8");
54
+ });
55
+
56
+ console.log(chalk.green(`✔ Mirrored ${capsules.length} capsules locally.`));
57
+ console.log(chalk.green(`Local capsule directory: ${CAPSULES_DIR}\n`));
58
+ }