interagent-framework 0.1.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.
- interagent/__init__.py +23 -0
- interagent/cli.py +982 -0
- interagent/constants.py +49 -0
- interagent/locking.py +147 -0
- interagent/messaging.py +183 -0
- interagent/session.py +129 -0
- interagent/task.py +204 -0
- interagent/templates/__init__.py +35 -0
- interagent/templates/review_request.md +68 -0
- interagent/templates/task_delegation.md +69 -0
- interagent/templates/update_prompt.md +70 -0
- interagent/utils.py +90 -0
- interagent/validator.py +156 -0
- interagent/watchdog.py +140 -0
- interagent_framework-0.1.0.dist-info/METADATA +588 -0
- interagent_framework-0.1.0.dist-info/RECORD +20 -0
- interagent_framework-0.1.0.dist-info/WHEEL +5 -0
- interagent_framework-0.1.0.dist-info/entry_points.txt +4 -0
- interagent_framework-0.1.0.dist-info/licenses/LICENSE +21 -0
- interagent_framework-0.1.0.dist-info/top_level.txt +1 -0
interagent/cli.py
ADDED
|
@@ -0,0 +1,982 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Command-line interface for InterAgent."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import sys
|
|
6
|
+
from datetime import date
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import List, Optional
|
|
9
|
+
|
|
10
|
+
from . import __version__
|
|
11
|
+
from .constants import VALID_AGENTS, VALID_MODES, INTERAGENT_DIR
|
|
12
|
+
from .session import Session
|
|
13
|
+
from .task import Task, TaskStatus
|
|
14
|
+
from .messaging import Message, MessageBus
|
|
15
|
+
from .locking import acquire_lock, release_lock, LockError
|
|
16
|
+
from .validator import validate_task, validate_message
|
|
17
|
+
from .templates import get_template
|
|
18
|
+
from .utils import (
|
|
19
|
+
ensure_dirs,
|
|
20
|
+
print_success,
|
|
21
|
+
print_warning,
|
|
22
|
+
print_error,
|
|
23
|
+
print_info,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def cmd_init(args: argparse.Namespace) -> int:
|
|
28
|
+
"""Initialize a new session."""
|
|
29
|
+
if INTERAGENT_DIR.exists() and not args.force:
|
|
30
|
+
print_warning(".interagent/ already exists. Use --force to overwrite.")
|
|
31
|
+
return 1
|
|
32
|
+
|
|
33
|
+
ensure_dirs()
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
session = Session.create(
|
|
37
|
+
name=args.project or "Unnamed Project",
|
|
38
|
+
principal=args.principal or "claude",
|
|
39
|
+
mode=args.mode or "hierarchical",
|
|
40
|
+
)
|
|
41
|
+
session.save()
|
|
42
|
+
|
|
43
|
+
# Create README
|
|
44
|
+
readme_path = INTERAGENT_DIR / "README.md"
|
|
45
|
+
with open(readme_path, "w", encoding="utf-8") as f:
|
|
46
|
+
f.write(f"""# InterAgent Session: {session.name}
|
|
47
|
+
|
|
48
|
+
**ID:** {session.id}
|
|
49
|
+
**Mode:** {session.mode}
|
|
50
|
+
**Principal:** {session.principal}
|
|
51
|
+
|
|
52
|
+
## Quick Commands
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# Check status
|
|
56
|
+
interagent status
|
|
57
|
+
|
|
58
|
+
# Create task
|
|
59
|
+
interagent task create --title "Task name" --assignee kimi
|
|
60
|
+
|
|
61
|
+
# List tasks
|
|
62
|
+
interagent task list
|
|
63
|
+
|
|
64
|
+
# Quick delegation
|
|
65
|
+
interagent quick --to kimi "Implement auth"
|
|
66
|
+
|
|
67
|
+
# Check inbox
|
|
68
|
+
interagent inbox --agent kimi
|
|
69
|
+
|
|
70
|
+
# Get relay prompt
|
|
71
|
+
interagent relay --to kimi
|
|
72
|
+
|
|
73
|
+
# Summary
|
|
74
|
+
interagent summary
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Files
|
|
78
|
+
|
|
79
|
+
- `session.json` - Session configuration
|
|
80
|
+
- `agents/` - Agent status
|
|
81
|
+
- `tasks/active/` - Active tasks
|
|
82
|
+
- `tasks/completed/` - Completed tasks
|
|
83
|
+
- `messages/pending/` - Unread messages
|
|
84
|
+
- `messages/archive/` - Message history
|
|
85
|
+
- `shared/` - Shared context and decisions
|
|
86
|
+
""")
|
|
87
|
+
|
|
88
|
+
print_success(f"Initialized session: {session.name}")
|
|
89
|
+
print(f" ID: {session.id}")
|
|
90
|
+
print(f" Mode: {session.mode}")
|
|
91
|
+
print(f" Principal: {session.principal}")
|
|
92
|
+
print(f"\n[DIR] Created .interagent/ directory")
|
|
93
|
+
print("\nNext steps:")
|
|
94
|
+
print("1. Edit .interagent/shared/context.md with project details")
|
|
95
|
+
print("2. Quick start: interagent quick --to kimi \"Your task\"")
|
|
96
|
+
return 0
|
|
97
|
+
|
|
98
|
+
except ValueError as e:
|
|
99
|
+
print_error(str(e))
|
|
100
|
+
return 1
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def cmd_status(args: argparse.Namespace) -> int:
|
|
104
|
+
"""Show session status."""
|
|
105
|
+
session = Session.load()
|
|
106
|
+
if not session:
|
|
107
|
+
print_error("No session found. Run: interagent init")
|
|
108
|
+
return 1
|
|
109
|
+
|
|
110
|
+
print(f"[STAT] Session: {session.name}")
|
|
111
|
+
print(f" ID: {session.id}")
|
|
112
|
+
print(f" Mode: {session.mode}")
|
|
113
|
+
print(f" Principal: {session.principal}")
|
|
114
|
+
|
|
115
|
+
print(f"\n[AGENTS] Agents:")
|
|
116
|
+
for agent, info in session.agents.items():
|
|
117
|
+
print(f" {agent}: {info.get('role', 'unknown')}")
|
|
118
|
+
|
|
119
|
+
# Count tasks
|
|
120
|
+
active_tasks = Task.list_all(active_only=True)
|
|
121
|
+
completed_tasks = Task.list_all()
|
|
122
|
+
completed_tasks = [t for t in completed_tasks if t.status in ["completed", "approved"]]
|
|
123
|
+
|
|
124
|
+
print(f"\n[TASK] Tasks:")
|
|
125
|
+
print(f" Active: {len(active_tasks)}")
|
|
126
|
+
print(f" Completed: {len(completed_tasks)}")
|
|
127
|
+
|
|
128
|
+
# Count messages
|
|
129
|
+
pending = MessageBus.get_inbox("claude") + MessageBus.get_inbox("kimi")
|
|
130
|
+
print(f"\n[MSG] Pending Messages: {len(pending)}")
|
|
131
|
+
|
|
132
|
+
return 0
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def cmd_summary(args: argparse.Namespace) -> int:
|
|
136
|
+
"""Show quick summary for relay decisions."""
|
|
137
|
+
session = Session.load()
|
|
138
|
+
if not session:
|
|
139
|
+
print_error("No session found. Run: interagent init")
|
|
140
|
+
return 1
|
|
141
|
+
|
|
142
|
+
print("=" * 60)
|
|
143
|
+
print("INTERAGENT SUMMARY")
|
|
144
|
+
print("=" * 60)
|
|
145
|
+
print()
|
|
146
|
+
|
|
147
|
+
# Session info
|
|
148
|
+
print(f"Session: {session.name} ({session.mode} mode)")
|
|
149
|
+
print(f"Principal: {session.principal}")
|
|
150
|
+
print()
|
|
151
|
+
|
|
152
|
+
# Tasks by status
|
|
153
|
+
all_tasks = Task.list_all()
|
|
154
|
+
|
|
155
|
+
pending_claude = [t for t in all_tasks if t.assignee == "claude" and t.status in ["pending", "assigned"]]
|
|
156
|
+
pending_kimi = [t for t in all_tasks if t.assignee == "kimi" and t.status in ["pending", "assigned"]]
|
|
157
|
+
in_progress_claude = [t for t in all_tasks if t.assignee == "claude" and t.status == "in_progress"]
|
|
158
|
+
in_progress_kimi = [t for t in all_tasks if t.assignee == "kimi" and t.status == "in_progress"]
|
|
159
|
+
ready_for_review = [t for t in all_tasks if t.status in ["completed", "under_review"]]
|
|
160
|
+
approved = [t for t in all_tasks if t.status == "approved"]
|
|
161
|
+
|
|
162
|
+
print("[TASKS]")
|
|
163
|
+
if pending_claude:
|
|
164
|
+
print(f" [WAIT] Claude: {len(pending_claude)} task(s) waiting to start")
|
|
165
|
+
if pending_kimi:
|
|
166
|
+
print(f" [WAIT] Kimi: {len(pending_kimi)} task(s) waiting to start")
|
|
167
|
+
if in_progress_claude:
|
|
168
|
+
print(f" [WORK] Claude: {len(in_progress_claude)} task(s) in progress")
|
|
169
|
+
if in_progress_kimi:
|
|
170
|
+
print(f" [WORK] Kimi: {len(in_progress_kimi)} task(s) in progress")
|
|
171
|
+
if ready_for_review:
|
|
172
|
+
print(f" [REVIEW] {len(ready_for_review)} task(s) ready for review")
|
|
173
|
+
if approved:
|
|
174
|
+
print(f" [OK] {len(approved)} task(s) approved")
|
|
175
|
+
|
|
176
|
+
if not any([pending_claude, pending_kimi, in_progress_claude, in_progress_kimi, ready_for_review, approved]):
|
|
177
|
+
print(" No active tasks")
|
|
178
|
+
print()
|
|
179
|
+
|
|
180
|
+
# Messages
|
|
181
|
+
claude_msgs = MessageBus.get_inbox("claude")
|
|
182
|
+
kimi_msgs = MessageBus.get_inbox("kimi")
|
|
183
|
+
|
|
184
|
+
print("[MESSAGES]")
|
|
185
|
+
if claude_msgs:
|
|
186
|
+
print(f" [MSG] Claude: {len(claude_msgs)} unread message(s)")
|
|
187
|
+
for msg in claude_msgs:
|
|
188
|
+
print(f" - From {msg.sender}: {msg.subject or '(no subject)'}")
|
|
189
|
+
if kimi_msgs:
|
|
190
|
+
print(f" [MSG] Kimi: {len(kimi_msgs)} unread message(s)")
|
|
191
|
+
for msg in kimi_msgs:
|
|
192
|
+
print(f" - From {msg.sender}: {msg.subject or '(no subject)'}")
|
|
193
|
+
if not claude_msgs and not kimi_msgs:
|
|
194
|
+
print(" No unread messages")
|
|
195
|
+
print()
|
|
196
|
+
|
|
197
|
+
# Action items
|
|
198
|
+
print("[ACTION ITEMS]")
|
|
199
|
+
if ready_for_review:
|
|
200
|
+
print(f" -> Tell {session.principal} to review {len(ready_for_review)} completed task(s)")
|
|
201
|
+
if pending_kimi and session.principal == "claude":
|
|
202
|
+
print(f" -> Tell Kimi to check inbox ({len(pending_kimi)} new task(s))")
|
|
203
|
+
if pending_claude and session.principal == "kimi":
|
|
204
|
+
print(f" -> Tell Claude to check inbox ({len(pending_claude)} new task(s))")
|
|
205
|
+
if claude_msgs:
|
|
206
|
+
print(" -> Tell Claude to check messages")
|
|
207
|
+
if kimi_msgs:
|
|
208
|
+
print(" -> Tell Kimi to check messages")
|
|
209
|
+
if not any([ready_for_review, pending_kimi, pending_claude, claude_msgs, kimi_msgs]):
|
|
210
|
+
print(" All caught up! No action needed.")
|
|
211
|
+
print()
|
|
212
|
+
|
|
213
|
+
# Quick commands
|
|
214
|
+
print("[QUICK COMMANDS]")
|
|
215
|
+
if ready_for_review:
|
|
216
|
+
task_id = ready_for_review[0].id
|
|
217
|
+
print(f" interagent task show {task_id}")
|
|
218
|
+
if kimi_msgs:
|
|
219
|
+
print(f" interagent relay --to kimi")
|
|
220
|
+
if claude_msgs:
|
|
221
|
+
print(f" interagent relay --to claude")
|
|
222
|
+
print()
|
|
223
|
+
|
|
224
|
+
return 0
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def cmd_relay(args: argparse.Namespace) -> int:
|
|
228
|
+
"""Generate relay prompt for an agent."""
|
|
229
|
+
agent = args.agent
|
|
230
|
+
|
|
231
|
+
# Get pending tasks for this agent
|
|
232
|
+
pending_tasks = Task.list_all(assignee=agent, status="assigned")
|
|
233
|
+
pending_tasks.extend(Task.list_all(assignee=agent, status="pending"))
|
|
234
|
+
|
|
235
|
+
# Get messages for this agent
|
|
236
|
+
messages = MessageBus.get_inbox(agent)
|
|
237
|
+
|
|
238
|
+
# Get session
|
|
239
|
+
session = Session.load()
|
|
240
|
+
role = session.get_agent_role(agent) if session else "delegate"
|
|
241
|
+
|
|
242
|
+
print("=" * 60)
|
|
243
|
+
print(f"RELAY PROMPT FOR {agent.upper()}")
|
|
244
|
+
print("=" * 60)
|
|
245
|
+
print()
|
|
246
|
+
print("Copy and paste this to the agent:")
|
|
247
|
+
print()
|
|
248
|
+
print("-" * 60)
|
|
249
|
+
print()
|
|
250
|
+
|
|
251
|
+
# Generate the prompt
|
|
252
|
+
print(f"@{agent} - You have work in the InterAgent collaboration system.")
|
|
253
|
+
print()
|
|
254
|
+
print(f"Your role: {role}")
|
|
255
|
+
print()
|
|
256
|
+
|
|
257
|
+
if pending_tasks:
|
|
258
|
+
print(f"[TASK] You have {len(pending_tasks)} new task(s):")
|
|
259
|
+
for task in pending_tasks:
|
|
260
|
+
print(f" - {task.title} ({task.id})")
|
|
261
|
+
print()
|
|
262
|
+
print("Please:")
|
|
263
|
+
print("1. Check .interagent/tasks/active/ for details")
|
|
264
|
+
print("2. Run: interagent task update <task_id> --status in_progress")
|
|
265
|
+
print("3. Do the work")
|
|
266
|
+
print("4. Run: interagent task update <task_id> --status completed")
|
|
267
|
+
print("5. Send a message when done: interagent msg send --to <other> --message 'Done!'")
|
|
268
|
+
print()
|
|
269
|
+
|
|
270
|
+
if messages:
|
|
271
|
+
print(f"[MSG] You have {len(messages)} unread message(s):")
|
|
272
|
+
for msg in messages[:3]: # Show first 3
|
|
273
|
+
print(f" From {msg.sender}: {msg.subject or '(no subject)'}")
|
|
274
|
+
print()
|
|
275
|
+
print("Check your inbox:")
|
|
276
|
+
print(f" interagent inbox --agent {agent}")
|
|
277
|
+
print()
|
|
278
|
+
|
|
279
|
+
if not pending_tasks and not messages:
|
|
280
|
+
print("No pending tasks or messages.")
|
|
281
|
+
print()
|
|
282
|
+
print("Useful commands:")
|
|
283
|
+
print(f" interagent status # Check overall status")
|
|
284
|
+
print(f" interagent summary # Quick summary")
|
|
285
|
+
print()
|
|
286
|
+
|
|
287
|
+
print("-" * 60)
|
|
288
|
+
print()
|
|
289
|
+
|
|
290
|
+
return 0
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def cmd_quick(args: argparse.Namespace) -> int:
|
|
294
|
+
"""Quick mode - single command for task delegation."""
|
|
295
|
+
ensure_dirs()
|
|
296
|
+
|
|
297
|
+
session = Session.load()
|
|
298
|
+
if not session:
|
|
299
|
+
print_error("No session found. Run: interagent init")
|
|
300
|
+
return 1
|
|
301
|
+
|
|
302
|
+
sender = args.from_agent or session.principal
|
|
303
|
+
recipient = args.to
|
|
304
|
+
task_desc = args.task
|
|
305
|
+
|
|
306
|
+
try:
|
|
307
|
+
# Create task with lock
|
|
308
|
+
task = Task.create(
|
|
309
|
+
title=task_desc[:100], # Limit title length
|
|
310
|
+
description=task_desc if len(task_desc) > 100 else "",
|
|
311
|
+
assignee=recipient,
|
|
312
|
+
assigner=sender,
|
|
313
|
+
priority=args.priority or "medium",
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
# Validate before saving
|
|
317
|
+
is_valid, errors = validate_task(task.to_dict())
|
|
318
|
+
if not is_valid:
|
|
319
|
+
print_error("Task validation failed:")
|
|
320
|
+
for err in errors:
|
|
321
|
+
print(f" - {err}")
|
|
322
|
+
return 1
|
|
323
|
+
|
|
324
|
+
# Try to acquire lock
|
|
325
|
+
try:
|
|
326
|
+
if not acquire_lock(f"task_{task.id}", timeout=5):
|
|
327
|
+
print_error("Could not create task - another process is working")
|
|
328
|
+
return 1
|
|
329
|
+
|
|
330
|
+
task.update(status="assigned")
|
|
331
|
+
task.save()
|
|
332
|
+
|
|
333
|
+
# Update session
|
|
334
|
+
session.add_task(task.id)
|
|
335
|
+
session.save()
|
|
336
|
+
|
|
337
|
+
finally:
|
|
338
|
+
release_lock(f"task_{task.id}")
|
|
339
|
+
|
|
340
|
+
# Create message
|
|
341
|
+
msg = Message.create(
|
|
342
|
+
sender=sender,
|
|
343
|
+
recipient=recipient,
|
|
344
|
+
subject=f"Task: {task.title}",
|
|
345
|
+
content=f"You have been assigned a task: {task_desc}\n\n"
|
|
346
|
+
f"Task ID: {task.id}\n"
|
|
347
|
+
f"Priority: {task.priority}\n\n"
|
|
348
|
+
f"To start: interagent task update {task.id} --status in_progress",
|
|
349
|
+
message_type="delegation",
|
|
350
|
+
task_id=task.id,
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
# Validate message
|
|
354
|
+
is_valid, errors = validate_message(msg.to_dict())
|
|
355
|
+
if is_valid:
|
|
356
|
+
MessageBus.send(msg)
|
|
357
|
+
|
|
358
|
+
print_success("Quick delegation complete!")
|
|
359
|
+
print(f" Task: {task.id}")
|
|
360
|
+
print(f" Assigned to: {recipient}")
|
|
361
|
+
print()
|
|
362
|
+
print("Next step:")
|
|
363
|
+
print(f" interagent relay --to {recipient}")
|
|
364
|
+
print()
|
|
365
|
+
print("This will generate the prompt to copy to the agent.")
|
|
366
|
+
|
|
367
|
+
return 0
|
|
368
|
+
|
|
369
|
+
except Exception as e:
|
|
370
|
+
print_error(f"Failed: {e}")
|
|
371
|
+
return 1
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def cmd_task_create(args: argparse.Namespace) -> int:
|
|
375
|
+
"""Create a new task."""
|
|
376
|
+
ensure_dirs()
|
|
377
|
+
|
|
378
|
+
try:
|
|
379
|
+
task = Task.create(
|
|
380
|
+
title=args.title,
|
|
381
|
+
description=args.description or "",
|
|
382
|
+
assignee=args.assignee,
|
|
383
|
+
assigner=args.assigner,
|
|
384
|
+
priority=args.priority or "medium",
|
|
385
|
+
requirements=args.requirements,
|
|
386
|
+
acceptance_criteria=args.criteria,
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
# Validate
|
|
390
|
+
is_valid, errors = validate_task(task.to_dict())
|
|
391
|
+
if not is_valid:
|
|
392
|
+
print_error("Task validation failed:")
|
|
393
|
+
for err in errors:
|
|
394
|
+
print(f" - {err}")
|
|
395
|
+
return 1
|
|
396
|
+
|
|
397
|
+
# Lock and save
|
|
398
|
+
if not acquire_lock(f"task_{task.id}", timeout=5):
|
|
399
|
+
print_error("Could not create task - another process is working")
|
|
400
|
+
return 1
|
|
401
|
+
|
|
402
|
+
try:
|
|
403
|
+
task.save()
|
|
404
|
+
|
|
405
|
+
# Update session
|
|
406
|
+
session = Session.load()
|
|
407
|
+
if session:
|
|
408
|
+
session.add_task(task.id)
|
|
409
|
+
session.save()
|
|
410
|
+
finally:
|
|
411
|
+
release_lock(f"task_{task.id}")
|
|
412
|
+
|
|
413
|
+
print_success(f"Created task: {task.id}")
|
|
414
|
+
print(f" Title: {task.title}")
|
|
415
|
+
print(f" Assignee: {task.assignee or 'Unassigned'}")
|
|
416
|
+
print(f" Priority: {task.priority}")
|
|
417
|
+
print(f"\n File: {INTERAGENT_DIR}/tasks/active/{task.id}.json")
|
|
418
|
+
return 0
|
|
419
|
+
|
|
420
|
+
except Exception as e:
|
|
421
|
+
print_error(f"Failed to create task: {e}")
|
|
422
|
+
return 1
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def cmd_task_list(args: argparse.Namespace) -> int:
|
|
426
|
+
"""List tasks."""
|
|
427
|
+
tasks = Task.list_all(
|
|
428
|
+
status=args.status,
|
|
429
|
+
assignee=args.assignee,
|
|
430
|
+
active_only=args.active_only,
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
if not tasks:
|
|
434
|
+
print_info("No tasks found.")
|
|
435
|
+
return 0
|
|
436
|
+
|
|
437
|
+
print(f"[TASK] Tasks ({len(tasks)}):")
|
|
438
|
+
print("-" * 80)
|
|
439
|
+
for task in tasks:
|
|
440
|
+
print(f"[{task.status:12}] {task.id}: {task.title}")
|
|
441
|
+
print(f" Assignee: {task.assignee or 'Unassigned'}")
|
|
442
|
+
print(f" Priority: {task.priority}")
|
|
443
|
+
print()
|
|
444
|
+
|
|
445
|
+
return 0
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def cmd_task_show(args: argparse.Namespace) -> int:
|
|
449
|
+
"""Show task details."""
|
|
450
|
+
task = Task.load(args.task_id)
|
|
451
|
+
if not task:
|
|
452
|
+
print_error(f"Task not found: {args.task_id}")
|
|
453
|
+
return 1
|
|
454
|
+
|
|
455
|
+
print(task.to_markdown())
|
|
456
|
+
return 0
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def cmd_task_update(args: argparse.Namespace) -> int:
|
|
460
|
+
"""Update task status."""
|
|
461
|
+
# Try to acquire lock
|
|
462
|
+
if not acquire_lock(f"task_{args.task_id}", timeout=10):
|
|
463
|
+
print_error("Task is currently being edited by another process")
|
|
464
|
+
return 1
|
|
465
|
+
|
|
466
|
+
try:
|
|
467
|
+
task = Task.load(args.task_id)
|
|
468
|
+
if not task:
|
|
469
|
+
print_error(f"Task not found: {args.task_id}")
|
|
470
|
+
return 1
|
|
471
|
+
|
|
472
|
+
if args.status:
|
|
473
|
+
old_status = task.status
|
|
474
|
+
task.update(status=args.status)
|
|
475
|
+
print(f"Status: {old_status} -> {args.status}")
|
|
476
|
+
|
|
477
|
+
# Move to completed if appropriate
|
|
478
|
+
if args.status in ["completed", "approved"]:
|
|
479
|
+
task.move_to_completed()
|
|
480
|
+
|
|
481
|
+
# Update session
|
|
482
|
+
session = Session.load()
|
|
483
|
+
if session:
|
|
484
|
+
session.complete_task(task.id)
|
|
485
|
+
session.save()
|
|
486
|
+
|
|
487
|
+
print("Moved to completed/")
|
|
488
|
+
|
|
489
|
+
if args.note:
|
|
490
|
+
notes = task.to_dict().get("notes", [])
|
|
491
|
+
from .utils import now_iso
|
|
492
|
+
notes.append({
|
|
493
|
+
"timestamp": now_iso(),
|
|
494
|
+
"note": args.note,
|
|
495
|
+
})
|
|
496
|
+
task.update(notes=notes)
|
|
497
|
+
print("Added note")
|
|
498
|
+
|
|
499
|
+
# Validate before saving
|
|
500
|
+
is_valid, errors = validate_task(task.to_dict())
|
|
501
|
+
if not is_valid:
|
|
502
|
+
print_error("Validation failed:")
|
|
503
|
+
for err in errors:
|
|
504
|
+
print(f" - {err}")
|
|
505
|
+
return 1
|
|
506
|
+
|
|
507
|
+
task.save()
|
|
508
|
+
print_success(f"Updated task: {args.task_id}")
|
|
509
|
+
return 0
|
|
510
|
+
|
|
511
|
+
finally:
|
|
512
|
+
release_lock(f"task_{args.task_id}")
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def cmd_msg_send(args: argparse.Namespace) -> int:
|
|
516
|
+
"""Send a message."""
|
|
517
|
+
ensure_dirs()
|
|
518
|
+
|
|
519
|
+
try:
|
|
520
|
+
message = Message.create(
|
|
521
|
+
sender=args.from_agent or "unknown",
|
|
522
|
+
recipient=args.to,
|
|
523
|
+
content=args.message,
|
|
524
|
+
subject=args.subject or "",
|
|
525
|
+
message_type=args.type or "message",
|
|
526
|
+
task_id=args.task_id,
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
# Validate
|
|
530
|
+
is_valid, errors = validate_message(message.to_dict())
|
|
531
|
+
if not is_valid:
|
|
532
|
+
print_error("Message validation failed:")
|
|
533
|
+
for err in errors:
|
|
534
|
+
print(f" - {err}")
|
|
535
|
+
return 1
|
|
536
|
+
|
|
537
|
+
MessageBus.send(message)
|
|
538
|
+
|
|
539
|
+
print_success(f"Message sent: {message.id}")
|
|
540
|
+
print(f" To: {args.to}")
|
|
541
|
+
print(f" Subject: {args.subject or '(no subject)'}")
|
|
542
|
+
print(f"\n @{args.to} - Check your inbox: interagent inbox --agent {args.to}")
|
|
543
|
+
return 0
|
|
544
|
+
|
|
545
|
+
except Exception as e:
|
|
546
|
+
print_error(f"Failed to send message: {e}")
|
|
547
|
+
return 1
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def cmd_inbox(args: argparse.Namespace) -> int:
|
|
551
|
+
"""Check inbox."""
|
|
552
|
+
agent = args.agent
|
|
553
|
+
if not agent:
|
|
554
|
+
# Try to determine current agent from session
|
|
555
|
+
session = Session.load()
|
|
556
|
+
if session:
|
|
557
|
+
print_info("Checking inbox for all agents...")
|
|
558
|
+
|
|
559
|
+
if agent:
|
|
560
|
+
messages = MessageBus.get_inbox(agent)
|
|
561
|
+
else:
|
|
562
|
+
messages = (
|
|
563
|
+
MessageBus.get_inbox("claude") +
|
|
564
|
+
MessageBus.get_inbox("kimi")
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
if not messages:
|
|
568
|
+
print_info(f"No messages for {agent or 'anyone'}")
|
|
569
|
+
return 0
|
|
570
|
+
|
|
571
|
+
print(f"[IN] Messages ({len(messages)}):")
|
|
572
|
+
print("-" * 80)
|
|
573
|
+
for msg in messages:
|
|
574
|
+
print(f"From: {msg.sender}")
|
|
575
|
+
print(f"To: {msg.recipient}")
|
|
576
|
+
print(f"Subject: {msg.subject or '(no subject)'}")
|
|
577
|
+
print(f"Time: {msg.timestamp}")
|
|
578
|
+
print(f"\n{msg.content}")
|
|
579
|
+
print("-" * 80)
|
|
580
|
+
|
|
581
|
+
return 0
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def cmd_msg_read(args: argparse.Namespace) -> int:
|
|
585
|
+
"""Mark message as read."""
|
|
586
|
+
if MessageBus.mark_read(args.msg_id):
|
|
587
|
+
print_success(f"Message archived: {args.msg_id}")
|
|
588
|
+
return 0
|
|
589
|
+
else:
|
|
590
|
+
print_error(f"Message not found: {args.msg_id}")
|
|
591
|
+
return 1
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def cmd_delegate(args: argparse.Namespace) -> int:
|
|
595
|
+
"""Quick delegation command."""
|
|
596
|
+
# Create task
|
|
597
|
+
task_id = f"task-xxx"
|
|
598
|
+
task = Task.create(
|
|
599
|
+
title=args.task,
|
|
600
|
+
description=args.description or "",
|
|
601
|
+
assignee=args.to,
|
|
602
|
+
assigner=args.from_agent or "claude",
|
|
603
|
+
priority=args.priority or "medium",
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
# Use quick command internally
|
|
607
|
+
class QuickArgs:
|
|
608
|
+
pass
|
|
609
|
+
|
|
610
|
+
quick_args = QuickArgs()
|
|
611
|
+
quick_args.from_agent = args.from_agent
|
|
612
|
+
quick_args.to = args.to
|
|
613
|
+
quick_args.task = args.task
|
|
614
|
+
quick_args.priority = args.priority
|
|
615
|
+
|
|
616
|
+
return cmd_quick(quick_args)
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def cmd_update_template(args: argparse.Namespace) -> int:
|
|
620
|
+
"""Generate a prompt instructing an agent to update the kickoff template."""
|
|
621
|
+
# Resolve template path
|
|
622
|
+
template_path = getattr(args, "template_path", None)
|
|
623
|
+
if not template_path:
|
|
624
|
+
# Walk up to 4 parent directories looking for template.txt
|
|
625
|
+
search = Path(__file__).parent
|
|
626
|
+
for _ in range(6):
|
|
627
|
+
candidate = search / "template.txt"
|
|
628
|
+
if candidate.exists():
|
|
629
|
+
template_path = str(candidate)
|
|
630
|
+
break
|
|
631
|
+
search = search.parent
|
|
632
|
+
if not template_path:
|
|
633
|
+
template_path = (
|
|
634
|
+
"~/Documents/projects/template.txt"
|
|
635
|
+
" (path not auto-detected - please verify)"
|
|
636
|
+
)
|
|
637
|
+
|
|
638
|
+
focus = getattr(args, "focus", None) or (
|
|
639
|
+
"all areas: new sub-agent capabilities, collaboration patterns, "
|
|
640
|
+
"updated Claude Code / Kimi Code features"
|
|
641
|
+
)
|
|
642
|
+
agent = args.agent
|
|
643
|
+
today = date.today().isoformat()
|
|
644
|
+
year = str(date.today().year)
|
|
645
|
+
|
|
646
|
+
try:
|
|
647
|
+
template = get_template("update_prompt")
|
|
648
|
+
except FileNotFoundError:
|
|
649
|
+
print_error("Template 'update_prompt' not found in src/interagent/templates/")
|
|
650
|
+
return 1
|
|
651
|
+
|
|
652
|
+
prompt = (
|
|
653
|
+
template
|
|
654
|
+
.replace("{agent}", agent.capitalize())
|
|
655
|
+
.replace("{template_path}", str(template_path))
|
|
656
|
+
.replace("{focus}", focus)
|
|
657
|
+
.replace("{date}", today)
|
|
658
|
+
.replace("{year}", year)
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
separator = "=" * 70
|
|
662
|
+
print(separator)
|
|
663
|
+
print(f"[PROMPT] Copy and paste the following into {agent.capitalize()} Code:")
|
|
664
|
+
print(separator)
|
|
665
|
+
print(prompt)
|
|
666
|
+
print(separator)
|
|
667
|
+
return 0
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
def create_parser() -> argparse.ArgumentParser:
|
|
671
|
+
"""Create argument parser."""
|
|
672
|
+
parser = argparse.ArgumentParser(
|
|
673
|
+
prog="interagent",
|
|
674
|
+
description="InterAgent - Framework for Claude and Kimi collaboration",
|
|
675
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
676
|
+
epilog="""
|
|
677
|
+
Examples:
|
|
678
|
+
interagent init --project "My API" --principal claude
|
|
679
|
+
interagent quick --to kimi "Implement authentication"
|
|
680
|
+
interagent relay --to kimi
|
|
681
|
+
interagent summary
|
|
682
|
+
interagent task list
|
|
683
|
+
interagent inbox --agent kimi
|
|
684
|
+
|
|
685
|
+
For more help: https://github.com/yourusername/interagent
|
|
686
|
+
""",
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
parser.add_argument(
|
|
690
|
+
"--version",
|
|
691
|
+
action="version",
|
|
692
|
+
version=f"%(prog)s {__version__}",
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
696
|
+
|
|
697
|
+
# Init
|
|
698
|
+
init_parser = subparsers.add_parser("init", help="Initialize session")
|
|
699
|
+
init_parser.add_argument("--project", "-p", help="Project name")
|
|
700
|
+
init_parser.add_argument(
|
|
701
|
+
"--principal",
|
|
702
|
+
choices=VALID_AGENTS,
|
|
703
|
+
default="claude",
|
|
704
|
+
help="Principal agent (default: claude)",
|
|
705
|
+
)
|
|
706
|
+
init_parser.add_argument(
|
|
707
|
+
"--mode",
|
|
708
|
+
choices=VALID_MODES,
|
|
709
|
+
default="hierarchical",
|
|
710
|
+
help="Collaboration mode (default: hierarchical)",
|
|
711
|
+
)
|
|
712
|
+
init_parser.add_argument(
|
|
713
|
+
"--force",
|
|
714
|
+
action="store_true",
|
|
715
|
+
help="Force overwrite existing session",
|
|
716
|
+
)
|
|
717
|
+
|
|
718
|
+
# Status
|
|
719
|
+
subparsers.add_parser("status", help="Show session status")
|
|
720
|
+
|
|
721
|
+
# Summary (NEW)
|
|
722
|
+
subparsers.add_parser("summary", help="Quick summary for relay decisions")
|
|
723
|
+
|
|
724
|
+
# Relay (NEW)
|
|
725
|
+
relay_parser = subparsers.add_parser("relay", help="Generate relay prompt for agent")
|
|
726
|
+
relay_parser.add_argument(
|
|
727
|
+
"--agent", "-a",
|
|
728
|
+
required=True,
|
|
729
|
+
choices=VALID_AGENTS,
|
|
730
|
+
help="Agent to generate prompt for",
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
# Quick (NEW)
|
|
734
|
+
quick_parser = subparsers.add_parser("quick", help="Quick task delegation (single command)")
|
|
735
|
+
quick_parser.add_argument(
|
|
736
|
+
"--to", "-t",
|
|
737
|
+
required=True,
|
|
738
|
+
choices=VALID_AGENTS,
|
|
739
|
+
help="Delegate to",
|
|
740
|
+
)
|
|
741
|
+
quick_parser.add_argument(
|
|
742
|
+
"--from-agent", "-f",
|
|
743
|
+
choices=VALID_AGENTS,
|
|
744
|
+
help="Delegate from",
|
|
745
|
+
)
|
|
746
|
+
quick_parser.add_argument(
|
|
747
|
+
"--priority",
|
|
748
|
+
choices=["low", "medium", "high", "critical"],
|
|
749
|
+
default="medium",
|
|
750
|
+
help="Task priority",
|
|
751
|
+
)
|
|
752
|
+
quick_parser.add_argument(
|
|
753
|
+
"task",
|
|
754
|
+
help="Task description",
|
|
755
|
+
)
|
|
756
|
+
|
|
757
|
+
# Task commands
|
|
758
|
+
task_parser = subparsers.add_parser("task", help="Task management")
|
|
759
|
+
task_subparsers = task_parser.add_subparsers(dest="task_command")
|
|
760
|
+
|
|
761
|
+
# Task create
|
|
762
|
+
task_create = task_subparsers.add_parser("create", help="Create task")
|
|
763
|
+
task_create.add_argument("--title", "-t", required=True, help="Task title")
|
|
764
|
+
task_create.add_argument("--description", "-d", help="Task description")
|
|
765
|
+
task_create.add_argument(
|
|
766
|
+
"--assignee", "-a",
|
|
767
|
+
choices=VALID_AGENTS,
|
|
768
|
+
help="Assign to agent",
|
|
769
|
+
)
|
|
770
|
+
task_create.add_argument(
|
|
771
|
+
"--assigner",
|
|
772
|
+
help="Assigned by agent",
|
|
773
|
+
)
|
|
774
|
+
task_create.add_argument(
|
|
775
|
+
"--priority",
|
|
776
|
+
choices=["low", "medium", "high", "critical"],
|
|
777
|
+
default="medium",
|
|
778
|
+
help="Task priority",
|
|
779
|
+
)
|
|
780
|
+
task_create.add_argument(
|
|
781
|
+
"--requirements",
|
|
782
|
+
nargs="+",
|
|
783
|
+
help="Task requirements",
|
|
784
|
+
)
|
|
785
|
+
task_create.add_argument(
|
|
786
|
+
"--criteria",
|
|
787
|
+
nargs="+",
|
|
788
|
+
help="Acceptance criteria",
|
|
789
|
+
)
|
|
790
|
+
|
|
791
|
+
# Task list
|
|
792
|
+
task_list = task_subparsers.add_parser("list", help="List tasks")
|
|
793
|
+
task_list.add_argument(
|
|
794
|
+
"--assignee",
|
|
795
|
+
choices=VALID_AGENTS,
|
|
796
|
+
help="Filter by assignee",
|
|
797
|
+
)
|
|
798
|
+
task_list.add_argument(
|
|
799
|
+
"--status",
|
|
800
|
+
help="Filter by status",
|
|
801
|
+
)
|
|
802
|
+
task_list.add_argument(
|
|
803
|
+
"--active-only",
|
|
804
|
+
action="store_true",
|
|
805
|
+
help="Show only active tasks",
|
|
806
|
+
)
|
|
807
|
+
|
|
808
|
+
# Task show
|
|
809
|
+
task_show = task_subparsers.add_parser("show", help="Show task details")
|
|
810
|
+
task_show.add_argument("task_id", help="Task ID")
|
|
811
|
+
|
|
812
|
+
# Task update
|
|
813
|
+
task_update = task_subparsers.add_parser("update", help="Update task")
|
|
814
|
+
task_update.add_argument("task_id", help="Task ID")
|
|
815
|
+
task_update.add_argument(
|
|
816
|
+
"--status",
|
|
817
|
+
choices=[
|
|
818
|
+
"pending", "assigned", "in_progress", "completed",
|
|
819
|
+
"under_review", "revision_needed", "approved", "rejected",
|
|
820
|
+
],
|
|
821
|
+
help="New status",
|
|
822
|
+
)
|
|
823
|
+
task_update.add_argument("--note", help="Add a note")
|
|
824
|
+
|
|
825
|
+
# Message commands
|
|
826
|
+
msg_parser = subparsers.add_parser("msg", help="Message management")
|
|
827
|
+
msg_subparsers = msg_parser.add_subparsers(dest="msg_command")
|
|
828
|
+
|
|
829
|
+
# Send message
|
|
830
|
+
msg_send = msg_subparsers.add_parser("send", help="Send a message")
|
|
831
|
+
msg_send.add_argument(
|
|
832
|
+
"--to", "-t",
|
|
833
|
+
required=True,
|
|
834
|
+
choices=VALID_AGENTS,
|
|
835
|
+
help="Recipient",
|
|
836
|
+
)
|
|
837
|
+
msg_send.add_argument(
|
|
838
|
+
"--from-agent", "-f",
|
|
839
|
+
choices=VALID_AGENTS,
|
|
840
|
+
help="Sender",
|
|
841
|
+
)
|
|
842
|
+
msg_send.add_argument("--subject", "-s", help="Message subject")
|
|
843
|
+
msg_send.add_argument(
|
|
844
|
+
"--message", "-m",
|
|
845
|
+
required=True,
|
|
846
|
+
help="Message content",
|
|
847
|
+
)
|
|
848
|
+
msg_send.add_argument(
|
|
849
|
+
"--type",
|
|
850
|
+
choices=["message", "delegation", "review", "discussion"],
|
|
851
|
+
default="message",
|
|
852
|
+
help="Message type",
|
|
853
|
+
)
|
|
854
|
+
msg_send.add_argument("--task-id", help="Related task ID")
|
|
855
|
+
|
|
856
|
+
# Read message
|
|
857
|
+
msg_read = msg_subparsers.add_parser("read", help="Mark message as read")
|
|
858
|
+
msg_read.add_argument("msg_id", help="Message ID")
|
|
859
|
+
|
|
860
|
+
# Inbox
|
|
861
|
+
inbox_parser = subparsers.add_parser("inbox", help="Check inbox")
|
|
862
|
+
inbox_parser.add_argument(
|
|
863
|
+
"--agent", "-a",
|
|
864
|
+
choices=VALID_AGENTS,
|
|
865
|
+
help="Check for specific agent",
|
|
866
|
+
)
|
|
867
|
+
|
|
868
|
+
# Delegate shortcut
|
|
869
|
+
delegate_parser = subparsers.add_parser("delegate", help="Quick task delegation")
|
|
870
|
+
delegate_parser.add_argument(
|
|
871
|
+
"--to", "-t",
|
|
872
|
+
required=True,
|
|
873
|
+
choices=VALID_AGENTS,
|
|
874
|
+
help="Delegate to",
|
|
875
|
+
)
|
|
876
|
+
delegate_parser.add_argument(
|
|
877
|
+
"--from-agent", "-f",
|
|
878
|
+
choices=VALID_AGENTS,
|
|
879
|
+
help="Delegate from",
|
|
880
|
+
)
|
|
881
|
+
delegate_parser.add_argument(
|
|
882
|
+
"--task",
|
|
883
|
+
required=True,
|
|
884
|
+
help="Task description",
|
|
885
|
+
)
|
|
886
|
+
delegate_parser.add_argument(
|
|
887
|
+
"--description", "-d",
|
|
888
|
+
help="Detailed description",
|
|
889
|
+
)
|
|
890
|
+
delegate_parser.add_argument(
|
|
891
|
+
"--priority",
|
|
892
|
+
choices=["low", "medium", "high", "critical"],
|
|
893
|
+
default="medium",
|
|
894
|
+
help="Task priority",
|
|
895
|
+
)
|
|
896
|
+
|
|
897
|
+
# update-template
|
|
898
|
+
update_tmpl_parser = subparsers.add_parser(
|
|
899
|
+
"update-template",
|
|
900
|
+
help="Generate a prompt to update the kickoff template with new AI best practices",
|
|
901
|
+
)
|
|
902
|
+
update_tmpl_parser.add_argument(
|
|
903
|
+
"--agent", "-a",
|
|
904
|
+
required=True,
|
|
905
|
+
choices=VALID_AGENTS,
|
|
906
|
+
help="Which agent receives and executes the update prompt (claude or kimi)",
|
|
907
|
+
)
|
|
908
|
+
update_tmpl_parser.add_argument(
|
|
909
|
+
"--template-path", "-p",
|
|
910
|
+
default=None,
|
|
911
|
+
dest="template_path",
|
|
912
|
+
help="Path to the template file (default: searches parent dirs for template.txt)",
|
|
913
|
+
)
|
|
914
|
+
update_tmpl_parser.add_argument(
|
|
915
|
+
"--focus", "-f",
|
|
916
|
+
default=None,
|
|
917
|
+
help="Optional focus area e.g. 'sub-agents', 'security', 'kimi-capabilities'",
|
|
918
|
+
)
|
|
919
|
+
|
|
920
|
+
return parser
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
def main(args: Optional[List[str]] = None) -> int:
|
|
924
|
+
"""Main entry point."""
|
|
925
|
+
parser = create_parser()
|
|
926
|
+
parsed_args = parser.parse_args(args)
|
|
927
|
+
|
|
928
|
+
if not parsed_args.command:
|
|
929
|
+
parser.print_help()
|
|
930
|
+
return 0
|
|
931
|
+
|
|
932
|
+
# Route commands
|
|
933
|
+
try:
|
|
934
|
+
if parsed_args.command == "init":
|
|
935
|
+
return cmd_init(parsed_args)
|
|
936
|
+
elif parsed_args.command == "status":
|
|
937
|
+
return cmd_status(parsed_args)
|
|
938
|
+
elif parsed_args.command == "summary":
|
|
939
|
+
return cmd_summary(parsed_args)
|
|
940
|
+
elif parsed_args.command == "relay":
|
|
941
|
+
return cmd_relay(parsed_args)
|
|
942
|
+
elif parsed_args.command == "quick":
|
|
943
|
+
return cmd_quick(parsed_args)
|
|
944
|
+
elif parsed_args.command == "task":
|
|
945
|
+
if parsed_args.task_command == "create":
|
|
946
|
+
return cmd_task_create(parsed_args)
|
|
947
|
+
elif parsed_args.task_command == "list":
|
|
948
|
+
return cmd_task_list(parsed_args)
|
|
949
|
+
elif parsed_args.task_command == "show":
|
|
950
|
+
return cmd_task_show(parsed_args)
|
|
951
|
+
elif parsed_args.task_command == "update":
|
|
952
|
+
return cmd_task_update(parsed_args)
|
|
953
|
+
else:
|
|
954
|
+
parser.parse_args(["task", "--help"])
|
|
955
|
+
return 0
|
|
956
|
+
elif parsed_args.command == "msg":
|
|
957
|
+
if parsed_args.msg_command == "send":
|
|
958
|
+
return cmd_msg_send(parsed_args)
|
|
959
|
+
elif parsed_args.msg_command == "read":
|
|
960
|
+
return cmd_msg_read(parsed_args)
|
|
961
|
+
else:
|
|
962
|
+
parser.parse_args(["msg", "--help"])
|
|
963
|
+
return 0
|
|
964
|
+
elif parsed_args.command == "inbox":
|
|
965
|
+
return cmd_inbox(parsed_args)
|
|
966
|
+
elif parsed_args.command == "delegate":
|
|
967
|
+
return cmd_delegate(parsed_args)
|
|
968
|
+
elif parsed_args.command == "update-template":
|
|
969
|
+
return cmd_update_template(parsed_args)
|
|
970
|
+
else:
|
|
971
|
+
parser.print_help()
|
|
972
|
+
return 0
|
|
973
|
+
except KeyboardInterrupt:
|
|
974
|
+
print("\nInterrupted.")
|
|
975
|
+
return 130
|
|
976
|
+
except Exception as e:
|
|
977
|
+
print_error(f"Unexpected error: {e}")
|
|
978
|
+
return 1
|
|
979
|
+
|
|
980
|
+
|
|
981
|
+
if __name__ == "__main__":
|
|
982
|
+
sys.exit(main())
|