@tjamescouch/agentchat 0.22.1 → 0.23.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/Dockerfile +1 -1
- package/dist/bin/agentchat.d.ts +7 -0
- package/dist/bin/agentchat.d.ts.map +1 -0
- package/dist/bin/agentchat.js +1511 -0
- package/dist/bin/agentchat.js.map +1 -0
- package/dist/lib/allowlist.d.ts +77 -0
- package/dist/lib/allowlist.d.ts.map +1 -0
- package/dist/lib/allowlist.js +151 -0
- package/dist/lib/allowlist.js.map +1 -0
- package/dist/lib/client.d.ts +147 -0
- package/dist/lib/client.d.ts.map +1 -0
- package/dist/lib/client.js +704 -0
- package/dist/lib/client.js.map +1 -0
- package/dist/lib/daemon.d.ts +122 -0
- package/dist/lib/daemon.d.ts.map +1 -0
- package/dist/lib/daemon.js +523 -0
- package/dist/lib/daemon.js.map +1 -0
- package/dist/lib/deploy/akash.d.ts +271 -0
- package/dist/lib/deploy/akash.d.ts.map +1 -0
- package/dist/lib/deploy/akash.js +671 -0
- package/dist/lib/deploy/akash.js.map +1 -0
- package/dist/lib/deploy/config.d.ts +62 -0
- package/dist/lib/deploy/config.d.ts.map +1 -0
- package/dist/lib/deploy/config.js +116 -0
- package/dist/lib/deploy/config.js.map +1 -0
- package/dist/lib/deploy/docker.d.ts +37 -0
- package/dist/lib/deploy/docker.d.ts.map +1 -0
- package/dist/lib/deploy/docker.js +122 -0
- package/dist/lib/deploy/docker.js.map +1 -0
- package/dist/lib/deploy/index.d.ts +11 -0
- package/dist/lib/deploy/index.d.ts.map +1 -0
- package/dist/lib/deploy/index.js +11 -0
- package/dist/lib/deploy/index.js.map +1 -0
- package/dist/lib/escrow-hooks.d.ts +199 -0
- package/dist/lib/escrow-hooks.d.ts.map +1 -0
- package/dist/lib/escrow-hooks.js +221 -0
- package/dist/lib/escrow-hooks.js.map +1 -0
- package/dist/lib/identity.d.ts +134 -0
- package/dist/lib/identity.d.ts.map +1 -0
- package/dist/lib/identity.js +334 -0
- package/dist/lib/identity.js.map +1 -0
- package/dist/lib/jitter.d.ts +42 -0
- package/dist/lib/jitter.d.ts.map +1 -0
- package/{lib/jitter.ts → dist/lib/jitter.js} +10 -18
- package/dist/lib/jitter.js.map +1 -0
- package/dist/lib/proposals.d.ts +223 -0
- package/dist/lib/proposals.d.ts.map +1 -0
- package/dist/lib/proposals.js +379 -0
- package/dist/lib/proposals.js.map +1 -0
- package/dist/lib/protocol.d.ts +220 -0
- package/dist/lib/protocol.d.ts.map +1 -0
- package/dist/lib/protocol.js +507 -0
- package/dist/lib/protocol.js.map +1 -0
- package/dist/lib/receipts.d.ts +134 -0
- package/dist/lib/receipts.d.ts.map +1 -0
- package/dist/lib/receipts.js +270 -0
- package/dist/lib/receipts.js.map +1 -0
- package/dist/lib/reputation.d.ts +250 -0
- package/dist/lib/reputation.d.ts.map +1 -0
- package/dist/lib/reputation.js +586 -0
- package/dist/lib/reputation.js.map +1 -0
- package/dist/lib/security.d.ts +27 -0
- package/dist/lib/security.d.ts.map +1 -0
- package/dist/lib/security.js +150 -0
- package/dist/lib/security.js.map +1 -0
- package/dist/lib/server/handlers/admin.d.ts +26 -0
- package/dist/lib/server/handlers/admin.d.ts.map +1 -0
- package/dist/lib/server/handlers/admin.js +76 -0
- package/dist/lib/server/handlers/admin.js.map +1 -0
- package/dist/lib/server/handlers/identity.d.ts +36 -0
- package/dist/lib/server/handlers/identity.d.ts.map +1 -0
- package/dist/lib/server/handlers/identity.js +330 -0
- package/dist/lib/server/handlers/identity.js.map +1 -0
- package/dist/lib/server/handlers/index.d.ts +10 -0
- package/dist/lib/server/handlers/index.d.ts.map +1 -0
- package/dist/lib/server/handlers/index.js +15 -0
- package/dist/lib/server/handlers/index.js.map +1 -0
- package/dist/lib/server/handlers/message.d.ts +47 -0
- package/dist/lib/server/handlers/message.d.ts.map +1 -0
- package/dist/lib/server/handlers/message.js +265 -0
- package/dist/lib/server/handlers/message.js.map +1 -0
- package/dist/lib/server/handlers/presence.d.ts +18 -0
- package/dist/lib/server/handlers/presence.d.ts.map +1 -0
- package/dist/lib/server/handlers/presence.js +35 -0
- package/dist/lib/server/handlers/presence.js.map +1 -0
- package/dist/lib/server/handlers/proposal.d.ts +38 -0
- package/dist/lib/server/handlers/proposal.d.ts.map +1 -0
- package/dist/lib/server/handlers/proposal.js +273 -0
- package/dist/lib/server/handlers/proposal.js.map +1 -0
- package/dist/lib/server/handlers/skills.d.ts +22 -0
- package/dist/lib/server/handlers/skills.d.ts.map +1 -0
- package/dist/lib/server/handlers/skills.js +119 -0
- package/dist/lib/server/handlers/skills.js.map +1 -0
- package/dist/lib/server-directory.d.ts +85 -0
- package/dist/lib/server-directory.d.ts.map +1 -0
- package/dist/lib/server-directory.js +177 -0
- package/dist/lib/server-directory.js.map +1 -0
- package/dist/lib/server.d.ts +162 -0
- package/dist/lib/server.d.ts.map +1 -0
- package/dist/lib/server.js +602 -0
- package/dist/lib/server.js.map +1 -0
- package/dist/lib/types.d.ts +461 -0
- package/dist/lib/types.d.ts.map +1 -0
- package/dist/lib/types.js +98 -0
- package/dist/lib/types.js.map +1 -0
- package/package.json +22 -13
- package/bin/agentchat.js +0 -1617
- package/bin/agentchat.ts +0 -1812
- package/lib/allowlist.js +0 -162
- package/lib/chat.py +0 -241
- package/lib/client.js +0 -821
- package/lib/client.ts +0 -877
- package/lib/daemon.js +0 -562
- package/lib/daemon.ts +0 -662
- package/lib/deploy/akash.js +0 -811
- package/lib/deploy/config.js +0 -128
- package/lib/deploy/docker.js +0 -132
- package/lib/deploy/index.js +0 -24
- package/lib/elo_swarm.py +0 -569
- package/lib/escrow-hooks.js +0 -237
- package/lib/escrow-hooks.ts +0 -391
- package/lib/identity.js +0 -376
- package/lib/identity.ts +0 -412
- package/lib/jitter.js +0 -54
- package/lib/proposals.js +0 -426
- package/lib/proposals.ts +0 -612
- package/lib/protocol.js +0 -516
- package/lib/receipts.js +0 -294
- package/lib/receipts.ts +0 -359
- package/lib/reputation.js +0 -664
- package/lib/reputation.ts +0 -790
- package/lib/security.js +0 -183
- package/lib/server/handlers/admin.js +0 -94
- package/lib/server/handlers/identity.js +0 -258
- package/lib/server/handlers/index.js +0 -42
- package/lib/server/handlers/message.js +0 -319
- package/lib/server/handlers/presence.js +0 -45
- package/lib/server/handlers/proposal.js +0 -358
- package/lib/server/handlers/skills.js +0 -141
- package/lib/server-directory.js +0 -190
- package/lib/server-directory.ts +0 -232
- package/lib/server.js +0 -633
- package/lib/server.ts +0 -698
- package/lib/supervisor/USAGE.md +0 -110
- package/lib/supervisor/agent-health.sh +0 -107
- package/lib/supervisor/agent-monitor.sh +0 -123
- package/lib/supervisor/agent-supervisor.sh +0 -135
- package/lib/supervisor/agentctl.sh +0 -266
- package/lib/supervisor/god-backup.sh +0 -126
- package/lib/supervisor/god-watchdog.sh +0 -107
- package/lib/supervisor/killswitch.sh +0 -43
- package/lib/supervisor/notify.sh +0 -19
- package/lib/types.ts +0 -433
package/lib/allowlist.js
DELETED
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Allowlist Module
|
|
3
|
-
* Controls which public keys can connect to the server.
|
|
4
|
-
* Opt-in via ALLOWLIST_ENABLED=true env var.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import crypto from 'crypto';
|
|
8
|
-
import fs from 'fs';
|
|
9
|
-
import path from 'path';
|
|
10
|
-
import { pubkeyToAgentId } from './protocol.js';
|
|
11
|
-
|
|
12
|
-
export class Allowlist {
|
|
13
|
-
constructor(options = {}) {
|
|
14
|
-
this.enabled = options.enabled || false;
|
|
15
|
-
this.strict = options.strict || false; // Block ephemeral connections when true
|
|
16
|
-
this.adminKey = options.adminKey || null;
|
|
17
|
-
this.filePath = options.filePath || path.join(process.cwd(), 'allowlist.json');
|
|
18
|
-
this.entries = new Map(); // pubkey -> { agentId, approvedAt, approvedBy, note }
|
|
19
|
-
|
|
20
|
-
if (this.enabled) {
|
|
21
|
-
this._load();
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Check if a pubkey is allowed to connect.
|
|
27
|
-
* Returns { allowed, reason }
|
|
28
|
-
*/
|
|
29
|
-
check(pubkey) {
|
|
30
|
-
if (!this.enabled) {
|
|
31
|
-
return { allowed: true, reason: 'allowlist disabled' };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (!pubkey) {
|
|
35
|
-
// Ephemeral connection (no pubkey)
|
|
36
|
-
if (this.strict) {
|
|
37
|
-
return { allowed: false, reason: 'ephemeral connections blocked in strict mode' };
|
|
38
|
-
}
|
|
39
|
-
return { allowed: true, reason: 'ephemeral allowed (non-strict mode)' };
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (this.entries.has(pubkey)) {
|
|
43
|
-
return { allowed: true, reason: 'pubkey approved' };
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return { allowed: false, reason: 'pubkey not in allowlist' };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Approve a pubkey for connection.
|
|
51
|
-
* Requires valid admin key.
|
|
52
|
-
*/
|
|
53
|
-
approve(pubkey, adminKey, note = '') {
|
|
54
|
-
if (!this._validateAdminKey(adminKey)) {
|
|
55
|
-
return { success: false, error: 'invalid admin key' };
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const agentId = pubkeyToAgentId(pubkey);
|
|
59
|
-
this.entries.set(pubkey, {
|
|
60
|
-
agentId,
|
|
61
|
-
approvedAt: new Date().toISOString(),
|
|
62
|
-
approvedBy: 'admin',
|
|
63
|
-
note,
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
this._save();
|
|
67
|
-
return { success: true, agentId };
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Revoke a pubkey from the allowlist.
|
|
72
|
-
* Can revoke by pubkey or agentId.
|
|
73
|
-
*/
|
|
74
|
-
revoke(identifier, adminKey) {
|
|
75
|
-
if (!this._validateAdminKey(adminKey)) {
|
|
76
|
-
return { success: false, error: 'invalid admin key' };
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Try by pubkey first
|
|
80
|
-
if (this.entries.has(identifier)) {
|
|
81
|
-
this.entries.delete(identifier);
|
|
82
|
-
this._save();
|
|
83
|
-
return { success: true };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Try by agentId
|
|
87
|
-
for (const [pubkey, entry] of this.entries) {
|
|
88
|
-
if (entry.agentId === identifier) {
|
|
89
|
-
this.entries.delete(pubkey);
|
|
90
|
-
this._save();
|
|
91
|
-
return { success: true };
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return { success: false, error: 'not found' };
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* List all approved entries.
|
|
100
|
-
*/
|
|
101
|
-
list() {
|
|
102
|
-
const result = [];
|
|
103
|
-
for (const [pubkey, entry] of this.entries) {
|
|
104
|
-
result.push({
|
|
105
|
-
agentId: `@${entry.agentId}`,
|
|
106
|
-
pubkeyPrefix: pubkey.slice(0, 40) + '...',
|
|
107
|
-
approvedAt: entry.approvedAt,
|
|
108
|
-
note: entry.note,
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
return result;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Validate admin key using timing-safe comparison.
|
|
116
|
-
* Hash both values first to ensure equal length (avoids length timing oracle).
|
|
117
|
-
*/
|
|
118
|
-
_validateAdminKey(key) {
|
|
119
|
-
if (!this.adminKey || !key) return false;
|
|
120
|
-
if (typeof key !== 'string') return false;
|
|
121
|
-
|
|
122
|
-
const a = crypto.createHash('sha256').update(this.adminKey).digest();
|
|
123
|
-
const b = crypto.createHash('sha256').update(key).digest();
|
|
124
|
-
return crypto.timingSafeEqual(a, b);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Load allowlist from disk.
|
|
129
|
-
*/
|
|
130
|
-
_load() {
|
|
131
|
-
try {
|
|
132
|
-
if (fs.existsSync(this.filePath)) {
|
|
133
|
-
const data = JSON.parse(fs.readFileSync(this.filePath, 'utf8'));
|
|
134
|
-
for (const entry of data) {
|
|
135
|
-
this.entries.set(entry.pubkey, {
|
|
136
|
-
agentId: entry.agentId,
|
|
137
|
-
approvedAt: entry.approvedAt,
|
|
138
|
-
approvedBy: entry.approvedBy || 'admin',
|
|
139
|
-
note: entry.note || '',
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
} catch (err) {
|
|
144
|
-
console.error(`Failed to load allowlist from ${this.filePath}: ${err.message}`);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Save allowlist to disk.
|
|
150
|
-
*/
|
|
151
|
-
_save() {
|
|
152
|
-
try {
|
|
153
|
-
const data = [];
|
|
154
|
-
for (const [pubkey, entry] of this.entries) {
|
|
155
|
-
data.push({ pubkey, ...entry });
|
|
156
|
-
}
|
|
157
|
-
fs.writeFileSync(this.filePath, JSON.stringify(data, null, 2));
|
|
158
|
-
} catch (err) {
|
|
159
|
-
console.error(`Failed to save allowlist to ${this.filePath}: ${err.message}`);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
package/lib/chat.py
DELETED
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""AgentChat daemon helper - read/send messages and track timestamps."""
|
|
3
|
-
|
|
4
|
-
import argparse
|
|
5
|
-
import json
|
|
6
|
-
import os
|
|
7
|
-
import sys
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
|
|
10
|
-
# Default daemon directory (can be overridden with --daemon-dir)
|
|
11
|
-
DEFAULT_DAEMON_DIR = Path.cwd() / ".agentchat" / "daemons" / "default"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def get_paths(daemon_dir: Path):
|
|
15
|
-
"""Get file paths for a daemon directory."""
|
|
16
|
-
return {
|
|
17
|
-
"inbox": daemon_dir / "inbox.jsonl",
|
|
18
|
-
"outbox": daemon_dir / "outbox.jsonl",
|
|
19
|
-
"last_ts": daemon_dir / "last_ts",
|
|
20
|
-
"newdata": daemon_dir / "newdata", # Semaphore for new messages
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def get_last_ts(paths: dict) -> int:
|
|
25
|
-
"""Get last seen timestamp."""
|
|
26
|
-
if paths["last_ts"].exists():
|
|
27
|
-
return int(paths["last_ts"].read_text().strip())
|
|
28
|
-
return 0
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def set_last_ts(paths: dict, ts: int) -> None:
|
|
32
|
-
"""Update last seen timestamp."""
|
|
33
|
-
paths["last_ts"].write_text(str(ts))
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def read_inbox(paths: dict, since_ts: int = None, limit: int = 50, include_replay: bool = False) -> list:
|
|
37
|
-
"""Read messages from inbox, optionally filtering by timestamp."""
|
|
38
|
-
if since_ts is None:
|
|
39
|
-
since_ts = get_last_ts(paths)
|
|
40
|
-
|
|
41
|
-
messages = []
|
|
42
|
-
if not paths["inbox"].exists():
|
|
43
|
-
return messages
|
|
44
|
-
|
|
45
|
-
with open(paths["inbox"]) as f:
|
|
46
|
-
for line in f:
|
|
47
|
-
line = line.strip()
|
|
48
|
-
if not line:
|
|
49
|
-
continue
|
|
50
|
-
try:
|
|
51
|
-
msg = json.loads(line)
|
|
52
|
-
ts = msg.get("ts", 0)
|
|
53
|
-
is_replay = msg.get("replay", False)
|
|
54
|
-
|
|
55
|
-
if ts > since_ts and (include_replay or not is_replay):
|
|
56
|
-
messages.append(msg)
|
|
57
|
-
except json.JSONDecodeError:
|
|
58
|
-
continue
|
|
59
|
-
|
|
60
|
-
# Sort by timestamp and limit
|
|
61
|
-
messages.sort(key=lambda m: m.get("ts", 0))
|
|
62
|
-
return messages[-limit:] if limit else messages
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def send_message(paths: dict, to: str, content: str) -> None:
|
|
66
|
-
"""Send a message by appending to outbox."""
|
|
67
|
-
msg = {"to": to, "content": content}
|
|
68
|
-
with open(paths["outbox"], "a") as f:
|
|
69
|
-
f.write(json.dumps(msg) + "\n")
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
def check_new(paths: dict, update_ts: bool = True) -> list:
|
|
73
|
-
"""Check for new messages and optionally update timestamp."""
|
|
74
|
-
messages = read_inbox(paths)
|
|
75
|
-
|
|
76
|
-
if messages and update_ts:
|
|
77
|
-
max_ts = max(m.get("ts", 0) for m in messages)
|
|
78
|
-
set_last_ts(paths, max_ts)
|
|
79
|
-
|
|
80
|
-
return messages
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def poll_new(paths: dict):
|
|
84
|
-
"""Poll for new messages using semaphore file. Returns None if no new data."""
|
|
85
|
-
# Fast path: check if semaphore exists
|
|
86
|
-
if not paths["newdata"].exists():
|
|
87
|
-
return None
|
|
88
|
-
|
|
89
|
-
# Semaphore exists, read messages
|
|
90
|
-
messages = read_inbox(paths)
|
|
91
|
-
|
|
92
|
-
# Delete semaphore
|
|
93
|
-
try:
|
|
94
|
-
paths["newdata"].unlink()
|
|
95
|
-
except FileNotFoundError:
|
|
96
|
-
pass # Race condition, already deleted
|
|
97
|
-
|
|
98
|
-
# Update timestamp if we got messages
|
|
99
|
-
if messages:
|
|
100
|
-
max_ts = max(m.get("ts", 0) for m in messages)
|
|
101
|
-
set_last_ts(paths, max_ts)
|
|
102
|
-
|
|
103
|
-
return messages
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
def wait_for_messages(paths: dict, interval: float = 2.0, timeout: float = 300.0):
|
|
107
|
-
"""Block until new messages arrive. Returns messages or empty list on timeout."""
|
|
108
|
-
import signal
|
|
109
|
-
import time
|
|
110
|
-
|
|
111
|
-
stop_file = paths["inbox"].parent.parent.parent / "stop"
|
|
112
|
-
|
|
113
|
-
# Handle interrupts gracefully
|
|
114
|
-
interrupted = False
|
|
115
|
-
def handle_signal(signum, frame):
|
|
116
|
-
nonlocal interrupted
|
|
117
|
-
interrupted = True
|
|
118
|
-
|
|
119
|
-
old_handler = signal.signal(signal.SIGINT, handle_signal)
|
|
120
|
-
|
|
121
|
-
try:
|
|
122
|
-
start = time.time()
|
|
123
|
-
while not interrupted and (time.time() - start) < timeout:
|
|
124
|
-
# Check stop file
|
|
125
|
-
if stop_file.exists():
|
|
126
|
-
try:
|
|
127
|
-
stop_file.unlink()
|
|
128
|
-
except FileNotFoundError:
|
|
129
|
-
pass
|
|
130
|
-
return [] # Signal to stop
|
|
131
|
-
|
|
132
|
-
# Check semaphore
|
|
133
|
-
if paths["newdata"].exists():
|
|
134
|
-
messages = read_inbox(paths)
|
|
135
|
-
# Filter out @server messages
|
|
136
|
-
messages = [m for m in messages if m.get("from") != "@server"]
|
|
137
|
-
|
|
138
|
-
if messages:
|
|
139
|
-
# Update timestamp
|
|
140
|
-
max_ts = max(m.get("ts", 0) for m in messages)
|
|
141
|
-
set_last_ts(paths, max_ts)
|
|
142
|
-
# Clear semaphore
|
|
143
|
-
try:
|
|
144
|
-
paths["newdata"].unlink()
|
|
145
|
-
except FileNotFoundError:
|
|
146
|
-
pass
|
|
147
|
-
return messages
|
|
148
|
-
|
|
149
|
-
# Semaphore but no messages after filtering - clear and continue
|
|
150
|
-
try:
|
|
151
|
-
paths["newdata"].unlink()
|
|
152
|
-
except FileNotFoundError:
|
|
153
|
-
pass
|
|
154
|
-
|
|
155
|
-
time.sleep(interval)
|
|
156
|
-
|
|
157
|
-
return [] # Timeout
|
|
158
|
-
finally:
|
|
159
|
-
signal.signal(signal.SIGINT, old_handler)
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
def main():
|
|
163
|
-
parser = argparse.ArgumentParser(description="AgentChat daemon helper")
|
|
164
|
-
parser.add_argument("--daemon-dir", type=Path, default=DEFAULT_DAEMON_DIR,
|
|
165
|
-
help="Daemon directory path")
|
|
166
|
-
|
|
167
|
-
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
168
|
-
|
|
169
|
-
# read command
|
|
170
|
-
read_p = subparsers.add_parser("read", help="Read inbox messages")
|
|
171
|
-
read_p.add_argument("--since", type=int, help="Only messages after this timestamp")
|
|
172
|
-
read_p.add_argument("--limit", type=int, default=50, help="Max messages to return")
|
|
173
|
-
read_p.add_argument("--replay", action="store_true", help="Include replay messages")
|
|
174
|
-
read_p.add_argument("--all", action="store_true", help="Read all messages (ignore last_ts)")
|
|
175
|
-
|
|
176
|
-
# send command
|
|
177
|
-
send_p = subparsers.add_parser("send", help="Send a message")
|
|
178
|
-
send_p.add_argument("to", help="Recipient (#channel or @agent)")
|
|
179
|
-
send_p.add_argument("content", help="Message content")
|
|
180
|
-
|
|
181
|
-
# check command - read new and update timestamp
|
|
182
|
-
check_p = subparsers.add_parser("check", help="Check for new messages and update timestamp")
|
|
183
|
-
check_p.add_argument("--no-update", action="store_true", help="Don't update last_ts")
|
|
184
|
-
|
|
185
|
-
# ts command - get/set timestamp
|
|
186
|
-
ts_p = subparsers.add_parser("ts", help="Get or set last timestamp")
|
|
187
|
-
ts_p.add_argument("value", nargs="?", type=int, help="Set timestamp to this value")
|
|
188
|
-
|
|
189
|
-
# poll command - efficient check using semaphore
|
|
190
|
-
poll_p = subparsers.add_parser("poll", help="Poll for new messages (uses semaphore, silent if none)")
|
|
191
|
-
|
|
192
|
-
# wait command - block until messages arrive
|
|
193
|
-
wait_p = subparsers.add_parser("wait", help="Block until new messages arrive")
|
|
194
|
-
wait_p.add_argument("--interval", type=float, default=2.0, help="Poll interval in seconds")
|
|
195
|
-
wait_p.add_argument("--timeout", type=float, default=300.0, help="Max wait time in seconds")
|
|
196
|
-
|
|
197
|
-
args = parser.parse_args()
|
|
198
|
-
paths = get_paths(args.daemon_dir)
|
|
199
|
-
|
|
200
|
-
if args.command == "read":
|
|
201
|
-
since = 0 if args.all else (args.since if args.since else get_last_ts(paths))
|
|
202
|
-
messages = read_inbox(paths, since_ts=since, limit=args.limit, include_replay=args.replay)
|
|
203
|
-
for msg in messages:
|
|
204
|
-
print(json.dumps(msg))
|
|
205
|
-
|
|
206
|
-
elif args.command == "send":
|
|
207
|
-
send_message(paths, args.to, args.content)
|
|
208
|
-
print(f"Sent to {args.to}")
|
|
209
|
-
|
|
210
|
-
elif args.command == "check":
|
|
211
|
-
messages = check_new(paths, update_ts=not args.no_update)
|
|
212
|
-
for msg in messages:
|
|
213
|
-
print(json.dumps(msg))
|
|
214
|
-
if not messages:
|
|
215
|
-
print("No new messages", file=sys.stderr)
|
|
216
|
-
|
|
217
|
-
elif args.command == "ts":
|
|
218
|
-
if args.value is not None:
|
|
219
|
-
set_last_ts(paths, args.value)
|
|
220
|
-
print(f"Set last_ts to {args.value}")
|
|
221
|
-
else:
|
|
222
|
-
print(get_last_ts(paths))
|
|
223
|
-
|
|
224
|
-
elif args.command == "poll":
|
|
225
|
-
messages = poll_new(paths)
|
|
226
|
-
if messages is None:
|
|
227
|
-
# No semaphore = no new data, exit silently
|
|
228
|
-
pass
|
|
229
|
-
elif messages:
|
|
230
|
-
for msg in messages:
|
|
231
|
-
print(json.dumps(msg))
|
|
232
|
-
# Empty list = semaphore existed but no new messages after filtering
|
|
233
|
-
|
|
234
|
-
elif args.command == "wait":
|
|
235
|
-
messages = wait_for_messages(paths, interval=args.interval, timeout=args.timeout)
|
|
236
|
-
for msg in messages:
|
|
237
|
-
print(json.dumps(msg))
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
if __name__ == "__main__":
|
|
241
|
-
main()
|