moltcli 0.1.0__py3-none-any.whl → 0.2.0__py3-none-any.whl
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.
- moltcli/cli.py +457 -39
- moltcli/core/__init__.py +2 -0
- moltcli/core/agent.py +60 -0
- moltcli/core/auth.py +4 -4
- moltcli/core/submolts.py +29 -2
- moltcli/core/vote.py +22 -8
- moltcli/utils/api_client.py +27 -4
- moltcli/utils/config.py +51 -1
- moltcli/utils/errors.py +16 -3
- {moltcli-0.1.0.dist-info → moltcli-0.2.0.dist-info}/METADATA +1 -1
- moltcli-0.2.0.dist-info/RECORD +22 -0
- moltcli-0.1.0.dist-info/RECORD +0 -21
- {moltcli-0.1.0.dist-info → moltcli-0.2.0.dist-info}/WHEEL +0 -0
- {moltcli-0.1.0.dist-info → moltcli-0.2.0.dist-info}/entry_points.txt +0 -0
- {moltcli-0.1.0.dist-info → moltcli-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {moltcli-0.1.0.dist-info → moltcli-0.2.0.dist-info}/top_level.txt +0 -0
moltcli/cli.py
CHANGED
|
@@ -13,12 +13,6 @@ from .core import (
|
|
|
13
13
|
)
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
def get_client() -> MoltbookClient:
|
|
17
|
-
"""Create API client from config."""
|
|
18
|
-
config = get_config()
|
|
19
|
-
return MoltbookClient(config.api_key)
|
|
20
|
-
|
|
21
|
-
|
|
22
16
|
def make_formatter(json_mode: bool) -> OutputFormatter:
|
|
23
17
|
"""Create output formatter."""
|
|
24
18
|
return OutputFormatter(json_mode=json_mode)
|
|
@@ -33,7 +27,20 @@ def cli(ctx: click.Context, json_mode: bool):
|
|
|
33
27
|
ctx.ensure_object(dict)
|
|
34
28
|
ctx.obj["json_mode"] = json_mode
|
|
35
29
|
ctx.obj["formatter"] = make_formatter(json_mode)
|
|
36
|
-
|
|
30
|
+
# Client is lazily loaded when needed (commands that require auth)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_client() -> MoltbookClient:
|
|
34
|
+
"""Create API client from config."""
|
|
35
|
+
config = get_config()
|
|
36
|
+
return MoltbookClient(config.api_key)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def ensure_client(ctx: click.Context) -> MoltbookClient:
|
|
40
|
+
"""Ensure client is available in context."""
|
|
41
|
+
if "client" not in ctx.obj or ctx.obj["client"] is None:
|
|
42
|
+
ctx.obj["client"] = get_client()
|
|
43
|
+
return ctx.obj["client"]
|
|
37
44
|
|
|
38
45
|
|
|
39
46
|
# auth command group
|
|
@@ -47,8 +54,8 @@ def auth():
|
|
|
47
54
|
@click.pass_context
|
|
48
55
|
def auth_whoami(ctx: click.Context):
|
|
49
56
|
"""Show current user info."""
|
|
50
|
-
client
|
|
51
|
-
formatter
|
|
57
|
+
client = ensure_client(ctx)
|
|
58
|
+
formatter = ctx.obj["formatter"]
|
|
52
59
|
try:
|
|
53
60
|
result = AuthCore(client).whoami()
|
|
54
61
|
formatter.print(result)
|
|
@@ -63,8 +70,8 @@ def auth_whoami(ctx: click.Context):
|
|
|
63
70
|
@click.pass_context
|
|
64
71
|
def auth_verify(ctx: click.Context):
|
|
65
72
|
"""Verify API key is valid."""
|
|
66
|
-
client
|
|
67
|
-
formatter
|
|
73
|
+
client = ensure_client(ctx)
|
|
74
|
+
formatter = ctx.obj["formatter"]
|
|
68
75
|
try:
|
|
69
76
|
result = AuthCore(client).verify()
|
|
70
77
|
formatter.print({"status": "valid", "message": "API key is valid"})
|
|
@@ -75,6 +82,289 @@ def auth_verify(ctx: click.Context):
|
|
|
75
82
|
raise
|
|
76
83
|
|
|
77
84
|
|
|
85
|
+
@auth.command("login")
|
|
86
|
+
@click.argument("api_key")
|
|
87
|
+
@click.option("--agent-name", help="Agent name (optional)")
|
|
88
|
+
@click.option("--json", "json_mode", is_flag=True, help="Output as JSON")
|
|
89
|
+
@click.pass_context
|
|
90
|
+
def auth_login(ctx: click.Context, api_key: str, agent_name: str, json_mode: bool):
|
|
91
|
+
"""Save API key to credentials file.
|
|
92
|
+
|
|
93
|
+
After registering, use this command to save your API key:
|
|
94
|
+
moltcli auth login moltbook_xxx
|
|
95
|
+
|
|
96
|
+
The credentials will be saved to ~/.config/moltbook/credentials.json
|
|
97
|
+
"""
|
|
98
|
+
from .utils import Config
|
|
99
|
+
formatter = OutputFormatter(json_mode=json_mode)
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
config = Config()
|
|
103
|
+
config.save(api_key, agent_name if agent_name else "")
|
|
104
|
+
formatter.print({
|
|
105
|
+
"status": "saved",
|
|
106
|
+
"config_path": str(config.config_path),
|
|
107
|
+
"message": "Credentials saved successfully"
|
|
108
|
+
})
|
|
109
|
+
if not json_mode:
|
|
110
|
+
click.echo(f"\nCredentials saved to: {config.config_path}")
|
|
111
|
+
click.echo("You can now use other moltcli commands.")
|
|
112
|
+
except FileExistsError as e:
|
|
113
|
+
if json_mode:
|
|
114
|
+
formatter.print({"status": "error", "message": str(e)})
|
|
115
|
+
else:
|
|
116
|
+
click.echo(f"Error: {e}", err=True)
|
|
117
|
+
sys.exit(1)
|
|
118
|
+
except Exception as e:
|
|
119
|
+
if json_mode:
|
|
120
|
+
formatter.print({"status": "error", "message": str(e)})
|
|
121
|
+
else:
|
|
122
|
+
click.echo(f"Error: {e}", err=True)
|
|
123
|
+
sys.exit(1)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@auth.command("logout")
|
|
127
|
+
@click.option("--json", "json_mode", is_flag=True, help="Output as JSON")
|
|
128
|
+
@click.pass_context
|
|
129
|
+
def auth_logout(ctx: click.Context, json_mode: bool):
|
|
130
|
+
"""Remove credentials file."""
|
|
131
|
+
from .utils import Config
|
|
132
|
+
formatter = OutputFormatter(json_mode=json_mode)
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
config = Config()
|
|
136
|
+
if config.exists():
|
|
137
|
+
config.remove()
|
|
138
|
+
formatter.print({
|
|
139
|
+
"status": "removed",
|
|
140
|
+
"message": "Credentials removed successfully"
|
|
141
|
+
})
|
|
142
|
+
if not json_mode:
|
|
143
|
+
click.echo("Credentials removed.")
|
|
144
|
+
else:
|
|
145
|
+
formatter.print({
|
|
146
|
+
"status": "skipped",
|
|
147
|
+
"message": "No credentials file found"
|
|
148
|
+
})
|
|
149
|
+
if not json_mode:
|
|
150
|
+
click.echo("No credentials file to remove.")
|
|
151
|
+
except Exception as e:
|
|
152
|
+
if json_mode:
|
|
153
|
+
formatter.print({"status": "error", "message": str(e)})
|
|
154
|
+
else:
|
|
155
|
+
click.echo(f"Error: {e}", err=True)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# Registration commands (no auth required)
|
|
159
|
+
@cli.command("register")
|
|
160
|
+
@click.argument("name")
|
|
161
|
+
@click.option("--description", default="", help="Agent description")
|
|
162
|
+
@click.option("--json", "json_mode", is_flag=True, help="Output as JSON")
|
|
163
|
+
def register(name: str, description: str, json_mode: bool):
|
|
164
|
+
"""Register a new agent.
|
|
165
|
+
|
|
166
|
+
This creates a new agent account. You'll receive an api_key, claim_url, and verification_code.
|
|
167
|
+
|
|
168
|
+
IMPORTANT: Save your api_key immediately! You need it for all future requests.
|
|
169
|
+
|
|
170
|
+
After registering, send the claim_url to your human to complete verification.
|
|
171
|
+
"""
|
|
172
|
+
from .utils import MoltbookClient, OutputFormatter, Config
|
|
173
|
+
from .core import AgentCore
|
|
174
|
+
|
|
175
|
+
client = MoltbookClient("") # No auth needed for register
|
|
176
|
+
formatter = OutputFormatter(json_mode=json_mode)
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
result = AgentCore(client).register(name, description)
|
|
180
|
+
formatter.print(result)
|
|
181
|
+
|
|
182
|
+
if "agent" in result:
|
|
183
|
+
agent = result["agent"]
|
|
184
|
+
api_key = agent.get("api_key")
|
|
185
|
+
claim_url = agent.get("claim_url")
|
|
186
|
+
verification_code = agent.get("verification_code")
|
|
187
|
+
|
|
188
|
+
# Auto-save credentials
|
|
189
|
+
config = Config()
|
|
190
|
+
config.save(api_key, name)
|
|
191
|
+
|
|
192
|
+
if json_mode:
|
|
193
|
+
formatter.print({
|
|
194
|
+
"status": "credentials_saved",
|
|
195
|
+
"config_path": str(config.config_path)
|
|
196
|
+
})
|
|
197
|
+
else:
|
|
198
|
+
click.echo("\n" + "=" * 50)
|
|
199
|
+
click.echo("Credentials saved to: " + str(config.config_path))
|
|
200
|
+
click.echo("=" * 50)
|
|
201
|
+
click.echo(f"\nNext steps:")
|
|
202
|
+
click.echo(f"1. Send this to your human to complete verification:")
|
|
203
|
+
click.echo(f" Claim URL: {claim_url}")
|
|
204
|
+
click.echo(f" Verification Code: {verification_code}")
|
|
205
|
+
click.echo(f"\n2. Your human will post a verification tweet,")
|
|
206
|
+
click.echo(f" then your account will be activated!")
|
|
207
|
+
click.echo(f"\n3. Check status with: moltcli status")
|
|
208
|
+
click.echo("=" * 50)
|
|
209
|
+
except FileExistsError:
|
|
210
|
+
if json_mode:
|
|
211
|
+
formatter.print({
|
|
212
|
+
"status": "error",
|
|
213
|
+
"message": "Credentials already exist. Use 'moltcli auth logout' first."
|
|
214
|
+
})
|
|
215
|
+
else:
|
|
216
|
+
click.echo("Error: Credentials already exist. Use 'moltcli auth logout' first.", err=True)
|
|
217
|
+
except Exception as e:
|
|
218
|
+
if json_mode:
|
|
219
|
+
formatter.print(handle_error(e))
|
|
220
|
+
else:
|
|
221
|
+
click.echo(f"Error: {e}", err=True)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@cli.command("status")
|
|
225
|
+
@click.option("--json", "json_mode", is_flag=True, help="Output as JSON")
|
|
226
|
+
@click.pass_context
|
|
227
|
+
def claim_status(ctx: click.Context, json_mode: bool):
|
|
228
|
+
"""Check your claim status (pending or claimed).
|
|
229
|
+
|
|
230
|
+
After registering, you need to be 'claimed' by your human via Twitter.
|
|
231
|
+
"""
|
|
232
|
+
from .utils import OutputFormatter
|
|
233
|
+
from .core import AgentCore
|
|
234
|
+
|
|
235
|
+
client = ensure_client(ctx)
|
|
236
|
+
formatter = OutputFormatter(json_mode=json_mode)
|
|
237
|
+
|
|
238
|
+
try:
|
|
239
|
+
result = AgentCore(client).get_status()
|
|
240
|
+
formatter.print(result)
|
|
241
|
+
|
|
242
|
+
status = result.get("status", "")
|
|
243
|
+
if not json_mode:
|
|
244
|
+
if status == "pending_claim":
|
|
245
|
+
click.echo("\nStatus: PENDING - Awaiting human verification")
|
|
246
|
+
elif status == "claimed":
|
|
247
|
+
click.echo("\nStatus: CLAIMED - Your account is active!")
|
|
248
|
+
else:
|
|
249
|
+
click.echo(f"\nStatus: {status}")
|
|
250
|
+
except Exception as e:
|
|
251
|
+
if json_mode:
|
|
252
|
+
formatter.print(handle_error(e))
|
|
253
|
+
else:
|
|
254
|
+
click.echo(f"Error: {e}", err=True)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
# agent command group
|
|
258
|
+
@cli.group()
|
|
259
|
+
def agent():
|
|
260
|
+
"""Agent profile and following."""
|
|
261
|
+
pass
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@agent.command("me")
|
|
265
|
+
@click.pass_context
|
|
266
|
+
def agent_me(ctx: click.Context):
|
|
267
|
+
"""Get current agent info."""
|
|
268
|
+
client = ensure_client(ctx)
|
|
269
|
+
formatter: OutputFormatter = ctx.obj["formatter"]
|
|
270
|
+
try:
|
|
271
|
+
result = AgentCore(client).get_me()
|
|
272
|
+
formatter.print(result)
|
|
273
|
+
except Exception as e:
|
|
274
|
+
if ctx.obj["json_mode"]:
|
|
275
|
+
formatter.print(handle_error(e))
|
|
276
|
+
sys.exit(1)
|
|
277
|
+
raise
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
@agent.command("profile")
|
|
281
|
+
@click.argument("name")
|
|
282
|
+
@click.pass_context
|
|
283
|
+
def agent_profile(ctx: click.Context, name: str):
|
|
284
|
+
"""Get another agent's profile."""
|
|
285
|
+
client = ensure_client(ctx)
|
|
286
|
+
formatter: OutputFormatter = ctx.obj["formatter"]
|
|
287
|
+
try:
|
|
288
|
+
result = AgentCore(client).get_profile(name)
|
|
289
|
+
formatter.print(result)
|
|
290
|
+
except Exception as e:
|
|
291
|
+
if ctx.obj["json_mode"]:
|
|
292
|
+
formatter.print(handle_error(e))
|
|
293
|
+
sys.exit(1)
|
|
294
|
+
raise
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
@agent.command("follow")
|
|
298
|
+
@click.argument("name")
|
|
299
|
+
@click.pass_context
|
|
300
|
+
def agent_follow(ctx: click.Context, name: str):
|
|
301
|
+
"""Follow an agent."""
|
|
302
|
+
client = ensure_client(ctx)
|
|
303
|
+
formatter: OutputFormatter = ctx.obj["formatter"]
|
|
304
|
+
try:
|
|
305
|
+
result = AgentCore(client).follow(name)
|
|
306
|
+
formatter.print({"status": "following", "agent": name})
|
|
307
|
+
except Exception as e:
|
|
308
|
+
if ctx.obj["json_mode"]:
|
|
309
|
+
formatter.print(handle_error(e))
|
|
310
|
+
sys.exit(1)
|
|
311
|
+
raise
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
@agent.command("unfollow")
|
|
315
|
+
@click.argument("name")
|
|
316
|
+
@click.pass_context
|
|
317
|
+
def agent_unfollow(ctx: click.Context, name: str):
|
|
318
|
+
"""Unfollow an agent."""
|
|
319
|
+
client = ensure_client(ctx)
|
|
320
|
+
formatter: OutputFormatter = ctx.obj["formatter"]
|
|
321
|
+
try:
|
|
322
|
+
result = AgentCore(client).unfollow(name)
|
|
323
|
+
formatter.print({"status": "unfollowed", "agent": name})
|
|
324
|
+
except Exception as e:
|
|
325
|
+
if ctx.obj["json_mode"]:
|
|
326
|
+
formatter.print(handle_error(e))
|
|
327
|
+
sys.exit(1)
|
|
328
|
+
raise
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
@agent.command("update")
|
|
332
|
+
@click.option("--description", help="Update your description")
|
|
333
|
+
@click.option("--metadata", help="Update metadata as JSON string")
|
|
334
|
+
@click.pass_context
|
|
335
|
+
def agent_update(ctx: click.Context, description: str, metadata: str):
|
|
336
|
+
"""Update your agent profile.
|
|
337
|
+
|
|
338
|
+
Note: You can only update one of description or metadata at a time.
|
|
339
|
+
"""
|
|
340
|
+
import json
|
|
341
|
+
client = ensure_client(ctx)
|
|
342
|
+
formatter: OutputFormatter = ctx.obj["formatter"]
|
|
343
|
+
|
|
344
|
+
meta_dict = None
|
|
345
|
+
if metadata:
|
|
346
|
+
try:
|
|
347
|
+
meta_dict = json.loads(metadata)
|
|
348
|
+
except json.JSONDecodeError:
|
|
349
|
+
if ctx.obj["json_mode"]:
|
|
350
|
+
formatter.print({"error": "Invalid JSON for --metadata"})
|
|
351
|
+
else:
|
|
352
|
+
click.echo("Error: Invalid JSON for --metadata", err=True)
|
|
353
|
+
return
|
|
354
|
+
|
|
355
|
+
try:
|
|
356
|
+
result = AgentCore(client).update_profile(
|
|
357
|
+
description=description if description else None,
|
|
358
|
+
metadata=meta_dict
|
|
359
|
+
)
|
|
360
|
+
formatter.print({"status": "updated"})
|
|
361
|
+
except Exception as e:
|
|
362
|
+
if ctx.obj["json_mode"]:
|
|
363
|
+
formatter.print(handle_error(e))
|
|
364
|
+
else:
|
|
365
|
+
click.echo(f"Error: {e}", err=True)
|
|
366
|
+
|
|
367
|
+
|
|
78
368
|
# post command group
|
|
79
369
|
@cli.group()
|
|
80
370
|
def post():
|
|
@@ -90,7 +380,7 @@ def post():
|
|
|
90
380
|
@click.pass_context
|
|
91
381
|
def post_create(ctx: click.Context, submolt: str, title: str, content: str, url: str):
|
|
92
382
|
"""Create a new post."""
|
|
93
|
-
client
|
|
383
|
+
client = ensure_client(ctx)
|
|
94
384
|
formatter: OutputFormatter = ctx.obj["formatter"]
|
|
95
385
|
try:
|
|
96
386
|
result = PostCore(client).create(submolt=submolt, title=title, content=content, url=url)
|
|
@@ -107,7 +397,7 @@ def post_create(ctx: click.Context, submolt: str, title: str, content: str, url:
|
|
|
107
397
|
@click.pass_context
|
|
108
398
|
def post_get(ctx: click.Context, post_id: str):
|
|
109
399
|
"""Get a post by ID."""
|
|
110
|
-
client
|
|
400
|
+
client = ensure_client(ctx)
|
|
111
401
|
formatter: OutputFormatter = ctx.obj["formatter"]
|
|
112
402
|
try:
|
|
113
403
|
result = PostCore(client).get(post_id)
|
|
@@ -124,7 +414,7 @@ def post_get(ctx: click.Context, post_id: str):
|
|
|
124
414
|
@click.pass_context
|
|
125
415
|
def post_delete(ctx: click.Context, post_id: str):
|
|
126
416
|
"""Delete a post."""
|
|
127
|
-
client
|
|
417
|
+
client = ensure_client(ctx)
|
|
128
418
|
formatter: OutputFormatter = ctx.obj["formatter"]
|
|
129
419
|
try:
|
|
130
420
|
result = PostCore(client).delete(post_id)
|
|
@@ -149,8 +439,11 @@ def comment():
|
|
|
149
439
|
@click.option("--parent", help="Parent comment ID for replies")
|
|
150
440
|
@click.pass_context
|
|
151
441
|
def comment_create(ctx: click.Context, post_id: str, content: str, parent: str):
|
|
152
|
-
"""Create a comment on a post.
|
|
153
|
-
|
|
442
|
+
"""Create a comment on a post.
|
|
443
|
+
|
|
444
|
+
Use --parent to reply to a specific comment.
|
|
445
|
+
"""
|
|
446
|
+
client = ensure_client(ctx)
|
|
154
447
|
formatter: OutputFormatter = ctx.obj["formatter"]
|
|
155
448
|
try:
|
|
156
449
|
result = CommentCore(client).create(post_id=post_id, content=content, parent_id=parent)
|
|
@@ -162,13 +455,32 @@ def comment_create(ctx: click.Context, post_id: str, content: str, parent: str):
|
|
|
162
455
|
raise
|
|
163
456
|
|
|
164
457
|
|
|
458
|
+
@comment.command("reply")
|
|
459
|
+
@click.argument("post_id")
|
|
460
|
+
@click.argument("parent_id")
|
|
461
|
+
@click.option("--content", required=True, help="Reply content")
|
|
462
|
+
@click.pass_context
|
|
463
|
+
def comment_reply(ctx: click.Context, post_id: str, parent_id: str, content: str):
|
|
464
|
+
"""Reply to a comment."""
|
|
465
|
+
client = ensure_client(ctx)
|
|
466
|
+
formatter: OutputFormatter = ctx.obj["formatter"]
|
|
467
|
+
try:
|
|
468
|
+
result = CommentCore(client).create(post_id=post_id, content=content, parent_id=parent_id)
|
|
469
|
+
formatter.print(result)
|
|
470
|
+
except Exception as e:
|
|
471
|
+
if ctx.obj["json_mode"]:
|
|
472
|
+
formatter.print(handle_error(e))
|
|
473
|
+
sys.exit(1)
|
|
474
|
+
raise
|
|
475
|
+
|
|
476
|
+
|
|
165
477
|
@comment.command("list")
|
|
166
478
|
@click.argument("post_id")
|
|
167
479
|
@click.option("--limit", default=50, help="Max comments to show")
|
|
168
480
|
@click.pass_context
|
|
169
481
|
def comment_list(ctx: click.Context, post_id: str, limit: int):
|
|
170
482
|
"""List comments for a post."""
|
|
171
|
-
client
|
|
483
|
+
client = ensure_client(ctx)
|
|
172
484
|
formatter: OutputFormatter = ctx.obj["formatter"]
|
|
173
485
|
try:
|
|
174
486
|
result = CommentCore(client).list_by_post(post_id, limit=limit)
|
|
@@ -187,14 +499,14 @@ def feed():
|
|
|
187
499
|
pass
|
|
188
500
|
|
|
189
501
|
|
|
190
|
-
@feed.command()
|
|
502
|
+
@feed.command("get")
|
|
191
503
|
@click.option("--sort", type=click.Choice(["hot", "new"]), default="hot", help="Sort order")
|
|
192
504
|
@click.option("--limit", default=20, help="Max posts to show")
|
|
193
505
|
@click.option("--submolt", help="Filter by submolt")
|
|
194
506
|
@click.pass_context
|
|
195
507
|
def feed_get(ctx: click.Context, sort: str, limit: int, submolt: str):
|
|
196
508
|
"""Get feed posts."""
|
|
197
|
-
client
|
|
509
|
+
client = ensure_client(ctx)
|
|
198
510
|
formatter: OutputFormatter = ctx.obj["formatter"]
|
|
199
511
|
try:
|
|
200
512
|
result = FeedCore(client).get(sort=sort, limit=limit, submolt=submolt)
|
|
@@ -211,7 +523,7 @@ def feed_get(ctx: click.Context, sort: str, limit: int, submolt: str):
|
|
|
211
523
|
@click.pass_context
|
|
212
524
|
def feed_hot(ctx: click.Context, limit: int):
|
|
213
525
|
"""Get hot posts."""
|
|
214
|
-
client
|
|
526
|
+
client = ensure_client(ctx)
|
|
215
527
|
formatter: OutputFormatter = ctx.obj["formatter"]
|
|
216
528
|
try:
|
|
217
529
|
result = FeedCore(client).get_hot(limit=limit)
|
|
@@ -228,7 +540,7 @@ def feed_hot(ctx: click.Context, limit: int):
|
|
|
228
540
|
@click.pass_context
|
|
229
541
|
def feed_new(ctx: click.Context, limit: int):
|
|
230
542
|
"""Get newest posts."""
|
|
231
|
-
client
|
|
543
|
+
client = ensure_client(ctx)
|
|
232
544
|
formatter: OutputFormatter = ctx.obj["formatter"]
|
|
233
545
|
try:
|
|
234
546
|
result = FeedCore(client).get_new(limit=limit)
|
|
@@ -247,17 +559,17 @@ def search():
|
|
|
247
559
|
pass
|
|
248
560
|
|
|
249
561
|
|
|
250
|
-
@search.command()
|
|
562
|
+
@search.command("query")
|
|
251
563
|
@click.argument("query")
|
|
252
|
-
@click.option("--type", type=click.Choice(["posts", "users"]), default="posts", help="Search type")
|
|
564
|
+
@click.option("--type", "search_type", type=click.Choice(["posts", "users"]), default="posts", help="Search type")
|
|
253
565
|
@click.option("--limit", default=20, help="Max results")
|
|
254
566
|
@click.pass_context
|
|
255
|
-
def search_query(ctx: click.Context, query: str,
|
|
567
|
+
def search_query(ctx: click.Context, query: str, search_type: str, limit: int):
|
|
256
568
|
"""Search posts or users."""
|
|
257
|
-
client
|
|
569
|
+
client = ensure_client(ctx)
|
|
258
570
|
formatter: OutputFormatter = ctx.obj["formatter"]
|
|
259
571
|
try:
|
|
260
|
-
result = SearchCore(client).search(query=query, type_=
|
|
572
|
+
result = SearchCore(client).search(query=query, type_=search_type, limit=limit)
|
|
261
573
|
formatter.print(result)
|
|
262
574
|
except Exception as e:
|
|
263
575
|
if ctx.obj["json_mode"]:
|
|
@@ -275,15 +587,15 @@ def vote():
|
|
|
275
587
|
|
|
276
588
|
@vote.command("up")
|
|
277
589
|
@click.argument("item_id")
|
|
278
|
-
@click.option("--type", type=click.Choice(["post", "comment"]), default="post", help="Item type")
|
|
590
|
+
@click.option("--type", "item_type", type=click.Choice(["post", "comment"]), default="post", help="Item type")
|
|
279
591
|
@click.pass_context
|
|
280
|
-
def vote_up(ctx: click.Context, item_id: str,
|
|
592
|
+
def vote_up(ctx: click.Context, item_id: str, item_type: str):
|
|
281
593
|
"""Upvote a post or comment."""
|
|
282
|
-
client
|
|
594
|
+
client = ensure_client(ctx)
|
|
283
595
|
formatter: OutputFormatter = ctx.obj["formatter"]
|
|
284
596
|
try:
|
|
285
|
-
result = VoteCore(client).upvote(item_id, type_=
|
|
286
|
-
formatter.print({"status": "upvoted", "id": item_id})
|
|
597
|
+
result = VoteCore(client).upvote(item_id, type_=item_type)
|
|
598
|
+
formatter.print({"status": "upvoted", "id": item_id, "type": item_type})
|
|
287
599
|
except Exception as e:
|
|
288
600
|
if ctx.obj["json_mode"]:
|
|
289
601
|
formatter.print(handle_error(e))
|
|
@@ -293,15 +605,49 @@ def vote_up(ctx: click.Context, item_id: str, type_: str):
|
|
|
293
605
|
|
|
294
606
|
@vote.command("down")
|
|
295
607
|
@click.argument("item_id")
|
|
296
|
-
@click.option("--type", type=click.Choice(["post", "comment"]), default="post", help="Item type")
|
|
608
|
+
@click.option("--type", "item_type", type=click.Choice(["post", "comment"]), default="post", help="Item type")
|
|
297
609
|
@click.pass_context
|
|
298
|
-
def vote_down(ctx: click.Context, item_id: str,
|
|
610
|
+
def vote_down(ctx: click.Context, item_id: str, item_type: str):
|
|
299
611
|
"""Downvote a post or comment."""
|
|
300
|
-
client
|
|
612
|
+
client = ensure_client(ctx)
|
|
613
|
+
formatter: OutputFormatter = ctx.obj["formatter"]
|
|
614
|
+
try:
|
|
615
|
+
result = VoteCore(client).downvote(item_id, type_=item_type)
|
|
616
|
+
formatter.print({"status": "downvoted", "id": item_id, "type": item_type})
|
|
617
|
+
except Exception as e:
|
|
618
|
+
if ctx.obj["json_mode"]:
|
|
619
|
+
formatter.print(handle_error(e))
|
|
620
|
+
sys.exit(1)
|
|
621
|
+
raise
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
@vote.command("up-comment")
|
|
625
|
+
@click.argument("comment_id")
|
|
626
|
+
@click.pass_context
|
|
627
|
+
def vote_up_comment(ctx: click.Context, comment_id: str):
|
|
628
|
+
"""Upvote a comment."""
|
|
629
|
+
client = ensure_client(ctx)
|
|
630
|
+
formatter: OutputFormatter = ctx.obj["formatter"]
|
|
631
|
+
try:
|
|
632
|
+
result = VoteCore(client).upvote_comment(comment_id)
|
|
633
|
+
formatter.print({"status": "upvoted", "id": comment_id, "type": "comment"})
|
|
634
|
+
except Exception as e:
|
|
635
|
+
if ctx.obj["json_mode"]:
|
|
636
|
+
formatter.print(handle_error(e))
|
|
637
|
+
sys.exit(1)
|
|
638
|
+
raise
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
@vote.command("down-comment")
|
|
642
|
+
@click.argument("comment_id")
|
|
643
|
+
@click.pass_context
|
|
644
|
+
def vote_down_comment(ctx: click.Context, comment_id: str):
|
|
645
|
+
"""Downvote a comment."""
|
|
646
|
+
client = ensure_client(ctx)
|
|
301
647
|
formatter: OutputFormatter = ctx.obj["formatter"]
|
|
302
648
|
try:
|
|
303
|
-
result = VoteCore(client).
|
|
304
|
-
formatter.print({"status": "downvoted", "id":
|
|
649
|
+
result = VoteCore(client).downvote_comment(comment_id)
|
|
650
|
+
formatter.print({"status": "downvoted", "id": comment_id, "type": "comment"})
|
|
305
651
|
except Exception as e:
|
|
306
652
|
if ctx.obj["json_mode"]:
|
|
307
653
|
formatter.print(handle_error(e))
|
|
@@ -321,7 +667,7 @@ def submolts():
|
|
|
321
667
|
@click.pass_context
|
|
322
668
|
def submolts_list(ctx: click.Context, limit: int):
|
|
323
669
|
"""List all submolts."""
|
|
324
|
-
client
|
|
670
|
+
client = ensure_client(ctx)
|
|
325
671
|
formatter: OutputFormatter = ctx.obj["formatter"]
|
|
326
672
|
try:
|
|
327
673
|
result = SubmoltsCore(client).list(limit=limit)
|
|
@@ -333,12 +679,67 @@ def submolts_list(ctx: click.Context, limit: int):
|
|
|
333
679
|
raise
|
|
334
680
|
|
|
335
681
|
|
|
682
|
+
@submolts.command("get")
|
|
683
|
+
@click.argument("name")
|
|
684
|
+
@click.pass_context
|
|
685
|
+
def submolts_get(ctx: click.Context, name: str):
|
|
686
|
+
"""Get submolt info."""
|
|
687
|
+
client = ensure_client(ctx)
|
|
688
|
+
formatter: OutputFormatter = ctx.obj["formatter"]
|
|
689
|
+
try:
|
|
690
|
+
result = SubmoltsCore(client).get(name)
|
|
691
|
+
formatter.print(result)
|
|
692
|
+
except Exception as e:
|
|
693
|
+
if ctx.obj["json_mode"]:
|
|
694
|
+
formatter.print(handle_error(e))
|
|
695
|
+
sys.exit(1)
|
|
696
|
+
raise
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
@submolts.command("create")
|
|
700
|
+
@click.argument("name")
|
|
701
|
+
@click.argument("display_name")
|
|
702
|
+
@click.option("--description", default="", help="Submolt description")
|
|
703
|
+
@click.pass_context
|
|
704
|
+
def submolts_create(ctx: click.Context, name: str, display_name: str, description: str):
|
|
705
|
+
"""Create a new submolt."""
|
|
706
|
+
client = ensure_client(ctx)
|
|
707
|
+
formatter: OutputFormatter = ctx.obj["formatter"]
|
|
708
|
+
try:
|
|
709
|
+
result = SubmoltsCore(client).create(name, display_name, description)
|
|
710
|
+
formatter.print({"status": "created", "name": name, "display_name": display_name})
|
|
711
|
+
except Exception as e:
|
|
712
|
+
if ctx.obj["json_mode"]:
|
|
713
|
+
formatter.print(handle_error(e))
|
|
714
|
+
sys.exit(1)
|
|
715
|
+
raise
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
@submolts.command("feed")
|
|
719
|
+
@click.argument("name")
|
|
720
|
+
@click.option("--sort", type=click.Choice(["hot", "new", "top", "rising"]), default="hot", help="Sort order")
|
|
721
|
+
@click.option("--limit", default=20, help="Max posts")
|
|
722
|
+
@click.pass_context
|
|
723
|
+
def submolts_feed(ctx: click.Context, name: str, sort: str, limit: int):
|
|
724
|
+
"""Get posts from a submolt."""
|
|
725
|
+
client = ensure_client(ctx)
|
|
726
|
+
formatter: OutputFormatter = ctx.obj["formatter"]
|
|
727
|
+
try:
|
|
728
|
+
result = SubmoltsCore(client).feed(name, sort=sort, limit=limit)
|
|
729
|
+
formatter.print(result)
|
|
730
|
+
except Exception as e:
|
|
731
|
+
if ctx.obj["json_mode"]:
|
|
732
|
+
formatter.print(handle_error(e))
|
|
733
|
+
sys.exit(1)
|
|
734
|
+
raise
|
|
735
|
+
|
|
736
|
+
|
|
336
737
|
@submolts.command("subscribe")
|
|
337
738
|
@click.argument("name")
|
|
338
739
|
@click.pass_context
|
|
339
740
|
def submolts_subscribe(ctx: click.Context, name: str):
|
|
340
741
|
"""Subscribe to a submolt."""
|
|
341
|
-
client
|
|
742
|
+
client = ensure_client(ctx)
|
|
342
743
|
formatter: OutputFormatter = ctx.obj["formatter"]
|
|
343
744
|
try:
|
|
344
745
|
result = SubmoltsCore(client).subscribe(name)
|
|
@@ -355,7 +756,7 @@ def submolts_subscribe(ctx: click.Context, name: str):
|
|
|
355
756
|
@click.pass_context
|
|
356
757
|
def submolts_unsubscribe(ctx: click.Context, name: str):
|
|
357
758
|
"""Unsubscribe from a submolt."""
|
|
358
|
-
client
|
|
759
|
+
client = ensure_client(ctx)
|
|
359
760
|
formatter: OutputFormatter = ctx.obj["formatter"]
|
|
360
761
|
try:
|
|
361
762
|
result = SubmoltsCore(client).unsubscribe(name)
|
|
@@ -367,6 +768,23 @@ def submolts_unsubscribe(ctx: click.Context, name: str):
|
|
|
367
768
|
raise
|
|
368
769
|
|
|
369
770
|
|
|
771
|
+
@submolts.command("trending")
|
|
772
|
+
@click.option("--limit", default=10, help="Max submolts to show")
|
|
773
|
+
@click.pass_context
|
|
774
|
+
def submolts_trending(ctx: click.Context, limit: int):
|
|
775
|
+
"""Get trending submolts."""
|
|
776
|
+
client = ensure_client(ctx)
|
|
777
|
+
formatter: OutputFormatter = ctx.obj["formatter"]
|
|
778
|
+
try:
|
|
779
|
+
result = SubmoltsCore(client).trending(limit=limit)
|
|
780
|
+
formatter.print(result)
|
|
781
|
+
except Exception as e:
|
|
782
|
+
if ctx.obj["json_mode"]:
|
|
783
|
+
formatter.print(handle_error(e))
|
|
784
|
+
sys.exit(1)
|
|
785
|
+
raise
|
|
786
|
+
|
|
787
|
+
|
|
370
788
|
def main():
|
|
371
789
|
"""Entry point."""
|
|
372
790
|
cli()
|
moltcli/core/__init__.py
CHANGED
|
@@ -7,6 +7,7 @@ from .search import SearchCore
|
|
|
7
7
|
from .vote import VoteCore
|
|
8
8
|
from .submolts import SubmoltsCore
|
|
9
9
|
from .auth import AuthCore
|
|
10
|
+
from .agent import AgentCore
|
|
10
11
|
|
|
11
12
|
__all__ = [
|
|
12
13
|
"PostCore",
|
|
@@ -16,4 +17,5 @@ __all__ = [
|
|
|
16
17
|
"VoteCore",
|
|
17
18
|
"SubmoltsCore",
|
|
18
19
|
"AuthCore",
|
|
20
|
+
"AgentCore",
|
|
19
21
|
]
|
moltcli/core/agent.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Agent core logic."""
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from ..utils.api_client import MoltbookClient
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AgentCore:
|
|
7
|
+
"""Handle agent operations."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, client: MoltbookClient):
|
|
10
|
+
self._client = client
|
|
11
|
+
|
|
12
|
+
def register(self, name: str, description: str = "") -> dict:
|
|
13
|
+
"""Register a new agent.
|
|
14
|
+
|
|
15
|
+
This does NOT require authentication - it's how new agents sign up.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
name: Unique agent name
|
|
19
|
+
description: Agent description
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Contains api_key, claim_url, and verification_code.
|
|
23
|
+
IMPORTANT: Save the api_key immediately!
|
|
24
|
+
"""
|
|
25
|
+
return self._client.post("/agents/register", json_data={
|
|
26
|
+
"name": name,
|
|
27
|
+
"description": description,
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
def get_status(self) -> dict:
|
|
31
|
+
"""Check claim status (pending or claimed)."""
|
|
32
|
+
return self._client.get("/agents/status")
|
|
33
|
+
|
|
34
|
+
def get_me(self) -> dict:
|
|
35
|
+
"""Get current agent info."""
|
|
36
|
+
return self._client.get("/agents/me")
|
|
37
|
+
|
|
38
|
+
def get_profile(self, name: str) -> dict:
|
|
39
|
+
"""Get another agent's profile."""
|
|
40
|
+
return self._client.get(f"/agents/profile?name={name}")
|
|
41
|
+
|
|
42
|
+
def follow(self, name: str) -> dict:
|
|
43
|
+
"""Follow an agent."""
|
|
44
|
+
return self._client.post(f"/agents/{name}/follow")
|
|
45
|
+
|
|
46
|
+
def unfollow(self, name: str) -> dict:
|
|
47
|
+
"""Unfollow an agent."""
|
|
48
|
+
return self._client.delete(f"/agents/{name}/follow")
|
|
49
|
+
|
|
50
|
+
def update_profile(self, description: str = None, metadata: dict = None) -> dict:
|
|
51
|
+
"""Update current agent profile.
|
|
52
|
+
|
|
53
|
+
Note: API only allows one of description or metadata at a time.
|
|
54
|
+
"""
|
|
55
|
+
data = {}
|
|
56
|
+
if description is not None:
|
|
57
|
+
data["description"] = description
|
|
58
|
+
if metadata is not None:
|
|
59
|
+
data["metadata"] = metadata
|
|
60
|
+
return self._client.patch("/agents/me", json_data=data)
|
moltcli/core/auth.py
CHANGED
|
@@ -9,12 +9,12 @@ class AuthCore:
|
|
|
9
9
|
self._client = client
|
|
10
10
|
|
|
11
11
|
def whoami(self) -> dict:
|
|
12
|
-
"""Get current user info."""
|
|
13
|
-
return self._client.get("/
|
|
12
|
+
"""Get current user info. Uses /agents/me endpoint."""
|
|
13
|
+
return self._client.get("/agents/me")
|
|
14
14
|
|
|
15
15
|
def verify(self) -> dict:
|
|
16
|
-
"""Verify API key is valid."""
|
|
17
|
-
return self._client.get("/
|
|
16
|
+
"""Verify API key is valid. Uses /agents/me endpoint."""
|
|
17
|
+
return self._client.get("/agents/me")
|
|
18
18
|
|
|
19
19
|
def refresh(self) -> dict:
|
|
20
20
|
"""Refresh authentication token."""
|
moltcli/core/submolts.py
CHANGED
|
@@ -17,13 +17,40 @@ class SubmoltsCore:
|
|
|
17
17
|
"""Get a submolt by name."""
|
|
18
18
|
return self._client.get(f"/submolts/{name}")
|
|
19
19
|
|
|
20
|
+
def create(self, name: str, display_name: str, description: str = "") -> dict:
|
|
21
|
+
"""Create a new submolt.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
name: Unique name (e.g., "aithoughts")
|
|
25
|
+
display_name: Display name (e.g., "AI Thoughts")
|
|
26
|
+
description: Submolt description
|
|
27
|
+
"""
|
|
28
|
+
return self._client.post("/submolts", json_data={
|
|
29
|
+
"name": name,
|
|
30
|
+
"display_name": display_name,
|
|
31
|
+
"description": description,
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
def feed(self, name: str, sort: str = "hot", limit: int = 20) -> dict:
|
|
35
|
+
"""Get feed from a specific submolt.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
name: Submolt name
|
|
39
|
+
sort: Sort order (hot, new, top, rising)
|
|
40
|
+
limit: Max posts to return
|
|
41
|
+
"""
|
|
42
|
+
return self._client.get(
|
|
43
|
+
f"/submolts/{name}/feed",
|
|
44
|
+
params={"sort": sort, "limit": limit}
|
|
45
|
+
)
|
|
46
|
+
|
|
20
47
|
def subscribe(self, name: str) -> dict:
|
|
21
48
|
"""Subscribe to a submolt."""
|
|
22
|
-
return self._client.post("/submolts/
|
|
49
|
+
return self._client.post(f"/submolts/{name}/subscribe")
|
|
23
50
|
|
|
24
51
|
def unsubscribe(self, name: str) -> dict:
|
|
25
52
|
"""Unsubscribe from a submolt."""
|
|
26
|
-
return self._client.
|
|
53
|
+
return self._client.delete(f"/submolts/{name}/subscribe")
|
|
27
54
|
|
|
28
55
|
def get_subscribed(self) -> dict:
|
|
29
56
|
"""Get user's subscribed submolts."""
|
moltcli/core/vote.py
CHANGED
|
@@ -20,11 +20,25 @@ class VoteCore:
|
|
|
20
20
|
return self._vote(item_id, self.DOWN, type_)
|
|
21
21
|
|
|
22
22
|
def _vote(self, item_id: str, direction: str, type_: str) -> dict:
|
|
23
|
-
"""Internal vote method.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
23
|
+
"""Internal vote method.
|
|
24
|
+
|
|
25
|
+
Endpoints from skill.md:
|
|
26
|
+
- POST /posts/{id}/upvote
|
|
27
|
+
- POST /posts/{id}/downvote
|
|
28
|
+
- POST /comments/{id}/upvote
|
|
29
|
+
- POST /comments/{id}/downvote
|
|
30
|
+
"""
|
|
31
|
+
direction_full = "upvote" if direction == "up" else "downvote"
|
|
32
|
+
if type_ == "post":
|
|
33
|
+
endpoint = f"/posts/{item_id}/{direction_full}"
|
|
34
|
+
else:
|
|
35
|
+
endpoint = f"/comments/{item_id}/{direction_full}"
|
|
36
|
+
return self._client.post(endpoint)
|
|
37
|
+
|
|
38
|
+
def upvote_comment(self, comment_id: str) -> dict:
|
|
39
|
+
"""Upvote a comment. Shortcut for: upvote(comment_id, type='comment')"""
|
|
40
|
+
return self._vote(comment_id, self.UP, "comment")
|
|
41
|
+
|
|
42
|
+
def downvote_comment(self, comment_id: str) -> dict:
|
|
43
|
+
"""Downvote a comment. Shortcut for: downvote(comment_id, type='comment')"""
|
|
44
|
+
return self._vote(comment_id, self.DOWN, "comment")
|
moltcli/utils/api_client.py
CHANGED
|
@@ -37,13 +37,21 @@ class MoltbookClient:
|
|
|
37
37
|
)
|
|
38
38
|
|
|
39
39
|
if not response.ok:
|
|
40
|
-
self._handle_error_response(response)
|
|
40
|
+
self._handle_error_response(response, endpoint)
|
|
41
41
|
|
|
42
42
|
return response.json()
|
|
43
43
|
|
|
44
|
-
def _handle_error_response(self, response):
|
|
44
|
+
def _handle_error_response(self, response, endpoint: str = ""):
|
|
45
45
|
"""Handle API error response with specific error types."""
|
|
46
46
|
status = response.status_code
|
|
47
|
+
response_text = response.text.strip()
|
|
48
|
+
|
|
49
|
+
# Determine endpoint type for rate limit
|
|
50
|
+
endpoint_type = "general"
|
|
51
|
+
if "/posts" in endpoint or "/post" in endpoint:
|
|
52
|
+
endpoint_type = "post"
|
|
53
|
+
elif "/vote" in endpoint or "/upvote" in endpoint or "/downvote" in endpoint:
|
|
54
|
+
endpoint_type = "vote"
|
|
47
55
|
|
|
48
56
|
# Rate limit (429)
|
|
49
57
|
if status == 429:
|
|
@@ -52,11 +60,12 @@ class MoltbookClient:
|
|
|
52
60
|
retry_after=retry_after,
|
|
53
61
|
limit=limit,
|
|
54
62
|
remaining=remaining,
|
|
63
|
+
endpoint_type=endpoint_type,
|
|
55
64
|
)
|
|
56
65
|
|
|
57
66
|
# Authentication errors (401, 403)
|
|
58
67
|
if status in (401, 403):
|
|
59
|
-
raise AuthError(f"Authentication failed: {response
|
|
68
|
+
raise AuthError(f"Authentication failed: {response_text or 'No response body'}")
|
|
60
69
|
|
|
61
70
|
# Not found (404)
|
|
62
71
|
if status == 404:
|
|
@@ -69,8 +78,18 @@ class MoltbookClient:
|
|
|
69
78
|
else:
|
|
70
79
|
raise NotFoundError("resource")
|
|
71
80
|
|
|
81
|
+
# Method not allowed (405)
|
|
82
|
+
if status == 405:
|
|
83
|
+
# Try to get error message from response body
|
|
84
|
+
try:
|
|
85
|
+
error_body = response.json()
|
|
86
|
+
error_msg = error_body.get("error", error_body.get("message", response_text))
|
|
87
|
+
except Exception:
|
|
88
|
+
error_msg = response_text or f"Method not allowed for {endpoint}"
|
|
89
|
+
raise Exception(f"API error {status}: {error_msg}")
|
|
90
|
+
|
|
72
91
|
# Other errors
|
|
73
|
-
raise Exception(f"API error
|
|
92
|
+
raise Exception(f"API error {status}: {response_text or 'No response body'}")
|
|
74
93
|
|
|
75
94
|
def get(self, endpoint: str, params: Optional[dict] = None) -> dict:
|
|
76
95
|
"""GET request."""
|
|
@@ -85,3 +104,7 @@ class MoltbookClient:
|
|
|
85
104
|
def delete(self, endpoint: str) -> dict:
|
|
86
105
|
"""DELETE request."""
|
|
87
106
|
return self._request("DELETE", endpoint)
|
|
107
|
+
|
|
108
|
+
def patch(self, endpoint: str, json_data: Optional[dict] = None) -> dict:
|
|
109
|
+
"""PATCH request."""
|
|
110
|
+
return self._request("PATCH", endpoint, json_data=json_data)
|
moltcli/utils/config.py
CHANGED
|
@@ -8,11 +8,14 @@ from typing import Optional
|
|
|
8
8
|
class Config:
|
|
9
9
|
"""Config loader for credentials and settings."""
|
|
10
10
|
|
|
11
|
+
DEFAULT_CONFIG_DIR = Path.home() / ".config" / "moltbook"
|
|
12
|
+
DEFAULT_CONFIG_FILE = DEFAULT_CONFIG_DIR / "credentials.json"
|
|
13
|
+
|
|
11
14
|
def __init__(self, config_path: Optional[str] = None):
|
|
12
15
|
if config_path is None:
|
|
13
16
|
config_path = os.environ.get(
|
|
14
17
|
"MOLTCLI_CONFIG_PATH",
|
|
15
|
-
str(
|
|
18
|
+
str(self.DEFAULT_CONFIG_FILE)
|
|
16
19
|
)
|
|
17
20
|
self.config_path = Path(config_path)
|
|
18
21
|
self._config: Optional[dict] = None
|
|
@@ -44,6 +47,53 @@ class Config:
|
|
|
44
47
|
"""Get config value by key."""
|
|
45
48
|
return self.config.get(key, default)
|
|
46
49
|
|
|
50
|
+
def save(self, api_key: str, agent_name: str = "") -> None:
|
|
51
|
+
"""Save credentials to config file.
|
|
52
|
+
|
|
53
|
+
Does NOT overwrite existing files.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
api_key: Moltbook API key
|
|
57
|
+
agent_name: Agent name (optional)
|
|
58
|
+
|
|
59
|
+
Raises:
|
|
60
|
+
FileExistsError: If config file already exists
|
|
61
|
+
"""
|
|
62
|
+
# Ensure config directory exists
|
|
63
|
+
self.config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
64
|
+
|
|
65
|
+
if self.config_path.exists():
|
|
66
|
+
raise FileExistsError(
|
|
67
|
+
f"Credentials file already exists: {self.config_path}\n"
|
|
68
|
+
f"Use 'moltcli auth logout' first to remove existing credentials."
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
config = {"api_key": api_key}
|
|
72
|
+
if agent_name:
|
|
73
|
+
config["agent_name"] = agent_name
|
|
74
|
+
|
|
75
|
+
with open(self.config_path, "x") as f:
|
|
76
|
+
json.dump(config, f, indent=2)
|
|
77
|
+
|
|
78
|
+
# Clear cached config
|
|
79
|
+
self._config = None
|
|
80
|
+
|
|
81
|
+
def remove(self) -> bool:
|
|
82
|
+
"""Remove config file.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
True if file was removed, False if it didn't exist.
|
|
86
|
+
"""
|
|
87
|
+
if self.config_path.exists():
|
|
88
|
+
self.config_path.unlink()
|
|
89
|
+
self._config = None
|
|
90
|
+
return True
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
def exists(self) -> bool:
|
|
94
|
+
"""Check if config file exists."""
|
|
95
|
+
return self.config_path.exists()
|
|
96
|
+
|
|
47
97
|
|
|
48
98
|
# Global config instance
|
|
49
99
|
_config: Optional[Config] = None
|
moltcli/utils/errors.py
CHANGED
|
@@ -51,11 +51,17 @@ class NotFoundError(MoltCLIError):
|
|
|
51
51
|
class RateLimitError(MoltCLIError):
|
|
52
52
|
"""Rate limit exceeded."""
|
|
53
53
|
|
|
54
|
+
# Default wait times for common scenarios
|
|
55
|
+
DEFAULT_WAIT_POST = 1800 # 30 minutes for posting
|
|
56
|
+
DEFAULT_WAIT_VOTE = 10 # 10 seconds for voting
|
|
57
|
+
DEFAULT_WAIT_GENERAL = 60 # 60 seconds for general requests
|
|
58
|
+
|
|
54
59
|
def __init__(
|
|
55
60
|
self,
|
|
56
61
|
retry_after: Optional[int] = None,
|
|
57
62
|
limit: Optional[int] = None,
|
|
58
63
|
remaining: Optional[int] = None,
|
|
64
|
+
endpoint_type: str = "general",
|
|
59
65
|
):
|
|
60
66
|
"""Initialize rate limit error.
|
|
61
67
|
|
|
@@ -63,22 +69,29 @@ class RateLimitError(MoltCLIError):
|
|
|
63
69
|
retry_after: Seconds until retry is allowed
|
|
64
70
|
limit: Rate limit maximum requests
|
|
65
71
|
remaining: Remaining requests in window
|
|
72
|
+
endpoint_type: Type of endpoint (post, vote, general)
|
|
66
73
|
"""
|
|
67
74
|
self.retry_after = retry_after
|
|
68
75
|
self.limit = limit
|
|
69
76
|
self.remaining = remaining
|
|
70
77
|
|
|
78
|
+
# Get default wait time based on endpoint type
|
|
79
|
+
default_wait = getattr(self, f"DEFAULT_WAIT_{endpoint_type.upper()}", self.DEFAULT_WAIT_GENERAL)
|
|
80
|
+
wait_time = retry_after or default_wait
|
|
81
|
+
|
|
71
82
|
# Build message
|
|
72
83
|
if retry_after:
|
|
73
84
|
message = f"Rate limit exceeded. Retry after {retry_after} seconds."
|
|
74
85
|
else:
|
|
75
|
-
message = "Rate limit exceeded. Please
|
|
86
|
+
message = f"Rate limit exceeded. Please wait about {wait_time} seconds."
|
|
76
87
|
|
|
77
88
|
# Build suggestion
|
|
78
|
-
if
|
|
89
|
+
if remaining is not None and remaining == 0:
|
|
90
|
+
suggestion = f"Rate limit reached (0/{limit} remaining). Wait {wait_time} seconds."
|
|
91
|
+
elif retry_after:
|
|
79
92
|
suggestion = f"Wait {retry_after} seconds before retrying."
|
|
80
93
|
else:
|
|
81
|
-
suggestion = "Wait
|
|
94
|
+
suggestion = f"Wait ~{wait_time} seconds before retrying. Consider adding --wait flag for auto-delay."
|
|
82
95
|
|
|
83
96
|
super().__init__(message, RATE_LIMIT, suggestion)
|
|
84
97
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
moltcli/__init__.py,sha256=e7p2Cat3JV5cvVi0A3XGjA8fhwg9p00mWktYc-RJcsM,77
|
|
2
|
+
moltcli/cli.py,sha256=UX5YqEpaSi0f3oMsYJy4wmmFr99dJlA3NYDV5OSovrg,24718
|
|
3
|
+
moltcli/core/__init__.py,sha256=JnkWgO3F3dYBKLOkwg1N7P2RDB31iR4g35gCtFkRMYA,425
|
|
4
|
+
moltcli/core/agent.py,sha256=bZ73jZxZhoNDXLOVwqthrXUiLgCD1u-1WNRemj3gsPA,1920
|
|
5
|
+
moltcli/core/auth.py,sha256=HmZH2KClOqTIhp93iL-fplbuECzQzILI8L2H4w16FSA,618
|
|
6
|
+
moltcli/core/comment.py,sha256=4zG-Zus3skOjBQklOg6D1wknFZyIyoywm1XgfzEXEIw,1090
|
|
7
|
+
moltcli/core/feed.py,sha256=GfXMP8ciLrh1_noNgeDntT0XjUQANMvbZ4xeLBjzK50,813
|
|
8
|
+
moltcli/core/post.py,sha256=bKIGn6NGDHyQoXekV1JCSIAsFGdGdXHmdpgDIjgY3Ho,1163
|
|
9
|
+
moltcli/core/search.py,sha256=_MkbaXXbAAEUL4pj_-E5P-AoAljSSuu59-o-PeYvWQs,832
|
|
10
|
+
moltcli/core/submolts.py,sha256=NVm_3Ld8MgP0XkiPqIqEJuzb88SiWr0jdlw8lVvWIYo,2005
|
|
11
|
+
moltcli/core/vote.py,sha256=Lle39DHCOqS7Paf6x3BRhfAxiGpwtkGRRZOKNvdbccs,1527
|
|
12
|
+
moltcli/utils/__init__.py,sha256=-bfVKSN8rxaSrTkb3oYG6wmD_GYoR7xn-IMo6v2WxeA,526
|
|
13
|
+
moltcli/utils/api_client.py,sha256=omNVpE-XlpjzBTQ5GU72B3gIUtlsPeesgxwttnn9vuM,3707
|
|
14
|
+
moltcli/utils/config.py,sha256=lDshmadh2j4jQdqR-GWJXH2-vypjd4vWnOoV32nxnYg,3078
|
|
15
|
+
moltcli/utils/errors.py,sha256=UmUEpeFpps2OSprtF83Qe49oQbVD8P0Exubmh4aIVBA,4835
|
|
16
|
+
moltcli/utils/formatter.py,sha256=kLYCPJw1IsanGYKXA8XGEm8mDAnC_AlIYQwN5bFypCc,1313
|
|
17
|
+
moltcli-0.2.0.dist-info/licenses/LICENSE,sha256=EOduU9kNb_dRzzPmt7gJsVSxqTCYJeYl5C2POxT0rj8,1069
|
|
18
|
+
moltcli-0.2.0.dist-info/METADATA,sha256=Zvruf9eFcX5AdPKyE9hGN4cpki_MSijQnf_-Lh2sljg,2585
|
|
19
|
+
moltcli-0.2.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
20
|
+
moltcli-0.2.0.dist-info/entry_points.txt,sha256=Cn6XRCOpVqQHJYqbjOCJ9LfePx6VYR_Gdn6MsoMtLq4,45
|
|
21
|
+
moltcli-0.2.0.dist-info/top_level.txt,sha256=3BuZXxTPE63yrySE7ET5eCy32LneFysqMiG-pfEQifw,8
|
|
22
|
+
moltcli-0.2.0.dist-info/RECORD,,
|
moltcli-0.1.0.dist-info/RECORD
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
moltcli/__init__.py,sha256=e7p2Cat3JV5cvVi0A3XGjA8fhwg9p00mWktYc-RJcsM,77
|
|
2
|
-
moltcli/cli.py,sha256=3S7QNzYxGJAaIs7dfYYIP73v75D6NukW7rTG_PUjnkE,11006
|
|
3
|
-
moltcli/core/__init__.py,sha256=Ow___Urg5hRu2AY0W9YNZihc8ILHmWLzE59BE-yR_lM,379
|
|
4
|
-
moltcli/core/auth.py,sha256=ZFpfQeE-Jyt75tHqb_eYJgwlUgb_sQzXO-rVNg5Q3qE,570
|
|
5
|
-
moltcli/core/comment.py,sha256=4zG-Zus3skOjBQklOg6D1wknFZyIyoywm1XgfzEXEIw,1090
|
|
6
|
-
moltcli/core/feed.py,sha256=GfXMP8ciLrh1_noNgeDntT0XjUQANMvbZ4xeLBjzK50,813
|
|
7
|
-
moltcli/core/post.py,sha256=bKIGn6NGDHyQoXekV1JCSIAsFGdGdXHmdpgDIjgY3Ho,1163
|
|
8
|
-
moltcli/core/search.py,sha256=_MkbaXXbAAEUL4pj_-E5P-AoAljSSuu59-o-PeYvWQs,832
|
|
9
|
-
moltcli/core/submolts.py,sha256=Fgq-AZQI03seWJGjTwN08OJM-n9_lQkY2oi6hG7N5Wg,1161
|
|
10
|
-
moltcli/core/vote.py,sha256=P5rbGnY-oeTf3nDBoKHDpkBA-jg8pm-X9re4JbYbEOk,966
|
|
11
|
-
moltcli/utils/__init__.py,sha256=-bfVKSN8rxaSrTkb3oYG6wmD_GYoR7xn-IMo6v2WxeA,526
|
|
12
|
-
moltcli/utils/api_client.py,sha256=Q5EZzvHwYE8Fr-G2_Yk3LrmKiAaCTP2eIhFT3blDwh8,2617
|
|
13
|
-
moltcli/utils/config.py,sha256=TctyM814QmSLDawQMPM2bNbac_3cto0Ewiao8Qt8w4U,1607
|
|
14
|
-
moltcli/utils/errors.py,sha256=8JmQ1SbP00DGl3XwGa0_zgqypiiVzHHkaw5r61CwV5U,4100
|
|
15
|
-
moltcli/utils/formatter.py,sha256=kLYCPJw1IsanGYKXA8XGEm8mDAnC_AlIYQwN5bFypCc,1313
|
|
16
|
-
moltcli-0.1.0.dist-info/licenses/LICENSE,sha256=EOduU9kNb_dRzzPmt7gJsVSxqTCYJeYl5C2POxT0rj8,1069
|
|
17
|
-
moltcli-0.1.0.dist-info/METADATA,sha256=wUIt8SHrXhA_sOVhsBRXHRgGVd0znEf-IhmyAYJdIEo,2585
|
|
18
|
-
moltcli-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
19
|
-
moltcli-0.1.0.dist-info/entry_points.txt,sha256=Cn6XRCOpVqQHJYqbjOCJ9LfePx6VYR_Gdn6MsoMtLq4,45
|
|
20
|
-
moltcli-0.1.0.dist-info/top_level.txt,sha256=3BuZXxTPE63yrySE7ET5eCy32LneFysqMiG-pfEQifw,8
|
|
21
|
-
moltcli-0.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|