@tjamescouch/agentchat 0.19.1 → 0.21.1
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/bin/agentchat.js +18 -0
- package/lib/client.js +14 -3
- package/lib/daemon.js +4 -0
- package/lib/security.js +183 -0
- package/lib/supervisor/USAGE.md +110 -0
- package/lib/supervisor/agent-supervisor.sh +135 -0
- package/lib/supervisor/agentctl.sh +250 -0
- package/lib/supervisor/killswitch.sh +36 -0
- package/lib/supervisor/notify.sh +19 -0
- package/package.json +2 -2
package/bin/agentchat.js
CHANGED
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
ServerDirectory,
|
|
53
53
|
DEFAULT_DIRECTORY_PATH
|
|
54
54
|
} from '../lib/server-directory.js';
|
|
55
|
+
import { enforceDirectorySafety, checkDirectorySafety } from '../lib/security.js';
|
|
55
56
|
|
|
56
57
|
program
|
|
57
58
|
.name('agentchat')
|
|
@@ -620,6 +621,12 @@ program
|
|
|
620
621
|
const instanceName = options.name;
|
|
621
622
|
const paths = getDaemonPaths(instanceName);
|
|
622
623
|
|
|
624
|
+
// Security check for operations that create files (not for status/list/stop)
|
|
625
|
+
const needsSafetyCheck = !options.list && !options.status && !options.stop && !options.stopAll;
|
|
626
|
+
if (needsSafetyCheck) {
|
|
627
|
+
enforceDirectorySafety(process.cwd(), { allowWarnings: true, silent: false });
|
|
628
|
+
}
|
|
629
|
+
|
|
623
630
|
// List all daemons
|
|
624
631
|
if (options.list) {
|
|
625
632
|
const instances = await listDaemons();
|
|
@@ -1546,6 +1553,17 @@ const firstArg = process.argv[2];
|
|
|
1546
1553
|
|
|
1547
1554
|
if (!firstArg || !subcommands.includes(firstArg)) {
|
|
1548
1555
|
// Launcher mode
|
|
1556
|
+
|
|
1557
|
+
// Security check: prevent running in root/system directories
|
|
1558
|
+
const safetyCheck = checkDirectorySafety(process.cwd());
|
|
1559
|
+
if (safetyCheck.level === 'error') {
|
|
1560
|
+
console.error(`\n❌ ERROR: ${safetyCheck.error}`);
|
|
1561
|
+
process.exit(1);
|
|
1562
|
+
}
|
|
1563
|
+
if (safetyCheck.level === 'warning') {
|
|
1564
|
+
console.error(`\n⚠️ WARNING: ${safetyCheck.warning}`);
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1549
1567
|
import('child_process').then(({ execSync, spawn }) => {
|
|
1550
1568
|
const name = firstArg; // May be undefined (anonymous) or a name
|
|
1551
1569
|
|
package/lib/client.js
CHANGED
|
@@ -43,16 +43,24 @@ export class AgentChatClient extends EventEmitter {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
|
-
* Load identity from file
|
|
46
|
+
* Load identity from file, or create new one if it doesn't exist
|
|
47
47
|
*/
|
|
48
48
|
async _loadIdentity() {
|
|
49
49
|
if (this.identityPath) {
|
|
50
50
|
try {
|
|
51
|
-
|
|
51
|
+
// Check if identity file exists
|
|
52
|
+
const exists = await Identity.exists(this.identityPath);
|
|
53
|
+
if (exists) {
|
|
54
|
+
this._identity = await Identity.load(this.identityPath);
|
|
55
|
+
} else {
|
|
56
|
+
// Generate new identity and save it
|
|
57
|
+
this._identity = Identity.generate(this.name);
|
|
58
|
+
await this._identity.save(this.identityPath);
|
|
59
|
+
}
|
|
52
60
|
this.name = this._identity.name;
|
|
53
61
|
this.pubkey = this._identity.pubkey;
|
|
54
62
|
} catch (err) {
|
|
55
|
-
throw new Error(`Failed to load identity
|
|
63
|
+
throw new Error(`Failed to load/create identity at ${this.identityPath}: ${err.message}`);
|
|
56
64
|
}
|
|
57
65
|
}
|
|
58
66
|
}
|
|
@@ -808,3 +816,6 @@ export async function listen(server, name, channels, callback, identityPath = nu
|
|
|
808
816
|
|
|
809
817
|
return client;
|
|
810
818
|
}
|
|
819
|
+
|
|
820
|
+
// Re-export security utilities
|
|
821
|
+
export { checkDirectorySafety, enforceDirectorySafety } from './security.js';
|
package/lib/daemon.js
CHANGED
|
@@ -11,6 +11,7 @@ import os from 'os';
|
|
|
11
11
|
import { AgentChatClient } from './client.js';
|
|
12
12
|
import { Identity, DEFAULT_IDENTITY_PATH } from './identity.js';
|
|
13
13
|
import { appendReceipt, shouldStoreReceipt, DEFAULT_RECEIPTS_PATH } from './receipts.js';
|
|
14
|
+
import { enforceDirectorySafety } from './security.js';
|
|
14
15
|
|
|
15
16
|
// Base directory (cwd-relative for project-local storage)
|
|
16
17
|
const AGENTCHAT_DIR = path.join(process.cwd(), '.agentchat');
|
|
@@ -341,6 +342,9 @@ export class AgentChatDaemon {
|
|
|
341
342
|
}
|
|
342
343
|
|
|
343
344
|
async start() {
|
|
345
|
+
// Security check: prevent running in root/system directories
|
|
346
|
+
enforceDirectorySafety(process.cwd(), { allowWarnings: true, silent: false });
|
|
347
|
+
|
|
344
348
|
this.running = true;
|
|
345
349
|
|
|
346
350
|
// Ensure instance directory exists
|
package/lib/security.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security utilities for AgentChat
|
|
3
|
+
* Prevents running agents in dangerous directories
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import os from 'os';
|
|
8
|
+
|
|
9
|
+
// Directories that are absolutely forbidden (system roots)
|
|
10
|
+
const FORBIDDEN_DIRECTORIES = new Set([
|
|
11
|
+
'/',
|
|
12
|
+
'/root',
|
|
13
|
+
'/home',
|
|
14
|
+
'/Users',
|
|
15
|
+
'/var',
|
|
16
|
+
'/etc',
|
|
17
|
+
'/usr',
|
|
18
|
+
'/bin',
|
|
19
|
+
'/sbin',
|
|
20
|
+
'/lib',
|
|
21
|
+
'/opt',
|
|
22
|
+
'/tmp',
|
|
23
|
+
'/System',
|
|
24
|
+
'/Applications',
|
|
25
|
+
'/Library',
|
|
26
|
+
'/private',
|
|
27
|
+
'/private/var',
|
|
28
|
+
'/private/tmp',
|
|
29
|
+
'C:\\',
|
|
30
|
+
'C:\\Windows',
|
|
31
|
+
'C:\\Program Files',
|
|
32
|
+
'C:\\Program Files (x86)',
|
|
33
|
+
'C:\\Users',
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
// Minimum depth from root required for a "project" directory
|
|
37
|
+
// e.g., /Users/name/projects/myproject = depth 4 (minimum required)
|
|
38
|
+
const MIN_SAFE_DEPTH = 3;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get the depth of a path from root
|
|
42
|
+
* @param {string} dirPath - Directory path to check
|
|
43
|
+
* @returns {number} - Number of directory levels from root
|
|
44
|
+
*/
|
|
45
|
+
function getPathDepth(dirPath) {
|
|
46
|
+
const normalized = path.normalize(dirPath);
|
|
47
|
+
const parts = normalized.split(path.sep).filter(p => p && p !== '.');
|
|
48
|
+
return parts.length;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Check if directory is a user's home directory
|
|
53
|
+
* @param {string} dirPath - Directory path to check
|
|
54
|
+
* @returns {boolean}
|
|
55
|
+
*/
|
|
56
|
+
function isHomeDirectory(dirPath) {
|
|
57
|
+
const normalized = path.normalize(dirPath);
|
|
58
|
+
const homeDir = os.homedir();
|
|
59
|
+
|
|
60
|
+
// Check exact match with home directory
|
|
61
|
+
if (normalized === homeDir || normalized === homeDir + path.sep) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Check common home directory patterns
|
|
66
|
+
const homePatterns = [
|
|
67
|
+
/^\/Users\/[^/]+$/, // macOS: /Users/username
|
|
68
|
+
/^\/home\/[^/]+$/, // Linux: /home/username
|
|
69
|
+
/^C:\\Users\\[^\\]+$/i, // Windows: C:\Users\username
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
return homePatterns.some(pattern => pattern.test(normalized));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Check if a directory is safe for running agentchat
|
|
77
|
+
* @param {string} dirPath - Directory path to check (defaults to cwd)
|
|
78
|
+
* @returns {{safe: boolean, error?: string, warning?: string, level: 'error'|'warning'|'ok'}}
|
|
79
|
+
*/
|
|
80
|
+
export function checkDirectorySafety(dirPath = process.cwd()) {
|
|
81
|
+
const normalized = path.normalize(path.resolve(dirPath));
|
|
82
|
+
|
|
83
|
+
// Check forbidden directories
|
|
84
|
+
if (FORBIDDEN_DIRECTORIES.has(normalized)) {
|
|
85
|
+
return {
|
|
86
|
+
safe: false,
|
|
87
|
+
level: 'error',
|
|
88
|
+
error: `Cannot run agentchat in system directory: ${normalized}\n` +
|
|
89
|
+
`Please run from a project directory instead.`
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Check if it's a home directory BEFORE depth check
|
|
94
|
+
// Home directories are allowed but warn (they're at depth 2 which would fail depth check)
|
|
95
|
+
if (isHomeDirectory(normalized)) {
|
|
96
|
+
return {
|
|
97
|
+
safe: true,
|
|
98
|
+
level: 'warning',
|
|
99
|
+
warning: `Running agentchat in home directory: ${normalized}\n` +
|
|
100
|
+
`Consider running from a specific project directory instead.`
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Check path depth (too shallow = too close to root)
|
|
105
|
+
const depth = getPathDepth(normalized);
|
|
106
|
+
if (depth < MIN_SAFE_DEPTH) {
|
|
107
|
+
return {
|
|
108
|
+
safe: false,
|
|
109
|
+
level: 'error',
|
|
110
|
+
error: `Cannot run agentchat in root-level directory: ${normalized}\n` +
|
|
111
|
+
`This directory is too close to the filesystem root.\n` +
|
|
112
|
+
`Please run from a project directory (at least ${MIN_SAFE_DEPTH} levels deep).`
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// All checks passed
|
|
117
|
+
return {
|
|
118
|
+
safe: true,
|
|
119
|
+
level: 'ok'
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Enforce directory safety check - throws if unsafe
|
|
125
|
+
* @param {string} dirPath - Directory path to check (defaults to cwd)
|
|
126
|
+
* @param {object} options - Options
|
|
127
|
+
* @param {boolean} options.allowWarnings - If true, don't throw on warnings (default: true)
|
|
128
|
+
* @param {boolean} options.silent - If true, don't print warnings (default: false)
|
|
129
|
+
* @throws {Error} If directory is not safe
|
|
130
|
+
*/
|
|
131
|
+
export function enforceDirectorySafety(dirPath = process.cwd(), options = {}) {
|
|
132
|
+
const { allowWarnings = true, silent = false } = options;
|
|
133
|
+
|
|
134
|
+
const result = checkDirectorySafety(dirPath);
|
|
135
|
+
|
|
136
|
+
if (result.level === 'error') {
|
|
137
|
+
throw new Error(result.error);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (result.level === 'warning') {
|
|
141
|
+
if (!silent) {
|
|
142
|
+
console.error(`\n⚠️ WARNING: ${result.warning}\n`);
|
|
143
|
+
}
|
|
144
|
+
if (!allowWarnings) {
|
|
145
|
+
throw new Error(result.warning);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Check if running in a project directory (has common project indicators)
|
|
154
|
+
* @param {string} dirPath - Directory path to check
|
|
155
|
+
* @returns {boolean}
|
|
156
|
+
*/
|
|
157
|
+
export function looksLikeProjectDirectory(dirPath = process.cwd()) {
|
|
158
|
+
const projectIndicators = [
|
|
159
|
+
'package.json',
|
|
160
|
+
'Cargo.toml',
|
|
161
|
+
'go.mod',
|
|
162
|
+
'pyproject.toml',
|
|
163
|
+
'setup.py',
|
|
164
|
+
'requirements.txt',
|
|
165
|
+
'Gemfile',
|
|
166
|
+
'pom.xml',
|
|
167
|
+
'build.gradle',
|
|
168
|
+
'Makefile',
|
|
169
|
+
'CMakeLists.txt',
|
|
170
|
+
'.git',
|
|
171
|
+
'.gitignore',
|
|
172
|
+
'README.md',
|
|
173
|
+
'README',
|
|
174
|
+
];
|
|
175
|
+
|
|
176
|
+
// This is a heuristic check - doesn't actually verify files exist
|
|
177
|
+
// Just checks if the path looks reasonable
|
|
178
|
+
const normalized = path.normalize(path.resolve(dirPath));
|
|
179
|
+
const depth = getPathDepth(normalized);
|
|
180
|
+
|
|
181
|
+
// If it's deep enough and not a system directory, it probably looks like a project
|
|
182
|
+
return depth >= MIN_SAFE_DEPTH && !FORBIDDEN_DIRECTORIES.has(normalized);
|
|
183
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Agent Supervisor System
|
|
2
|
+
|
|
3
|
+
Robust daemon management for Claude agents with automatic restart and state persistence.
|
|
4
|
+
|
|
5
|
+
## Why This Exists
|
|
6
|
+
|
|
7
|
+
Claude's built-in resume is unreliable. This system:
|
|
8
|
+
- Saves agent state externally to files
|
|
9
|
+
- Auto-restarts with exponential backoff
|
|
10
|
+
- Feeds previous context back on restart
|
|
11
|
+
- Lets agents save their own state before shutdown
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Add to PATH
|
|
17
|
+
export PATH="$PATH:$HOME/dev/claude/agentchat/lib/supervisor"
|
|
18
|
+
|
|
19
|
+
# Start an agent
|
|
20
|
+
agentctl start monitor "monitor agentchat #general, respond to messages, moderate spam"
|
|
21
|
+
|
|
22
|
+
# Check status
|
|
23
|
+
agentctl status
|
|
24
|
+
|
|
25
|
+
# View logs
|
|
26
|
+
agentctl logs monitor
|
|
27
|
+
|
|
28
|
+
# Stop gracefully
|
|
29
|
+
agentctl stop monitor
|
|
30
|
+
|
|
31
|
+
# Force kill
|
|
32
|
+
agentctl kill monitor
|
|
33
|
+
|
|
34
|
+
# Stop all agents
|
|
35
|
+
agentctl stopall
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Agent Self-Persistence
|
|
39
|
+
|
|
40
|
+
Inside your agent prompt, include instructions like:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
IMPORTANT: You are running under a supervisor that will restart you on failure.
|
|
44
|
+
|
|
45
|
+
Your state directory: ~/.agentchat/agents/YOUR_NAME/
|
|
46
|
+
- context.md: Save important state here BEFORE doing risky operations
|
|
47
|
+
- Read this file on startup to resume your work
|
|
48
|
+
|
|
49
|
+
Before any operation that might fail:
|
|
50
|
+
1. Write current task to context.md
|
|
51
|
+
2. Do the operation
|
|
52
|
+
3. Update context.md with result
|
|
53
|
+
|
|
54
|
+
On quota warnings or before shutdown:
|
|
55
|
+
- Save everything important to context.md
|
|
56
|
+
- Exit gracefully (the supervisor will restart you)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## File Structure
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
~/.agentchat/agents/
|
|
63
|
+
└── <agent-name>/
|
|
64
|
+
├── supervisor.pid # Supervisor process ID
|
|
65
|
+
├── state.json # Current state (managed by supervisor)
|
|
66
|
+
├── mission.txt # Original mission prompt
|
|
67
|
+
├── context.md # Agent-managed context (survives restarts)
|
|
68
|
+
└── supervisor.log # Supervisor logs
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Backoff Strategy
|
|
72
|
+
|
|
73
|
+
- Starts at 5 seconds
|
|
74
|
+
- Doubles on each failure (5 → 10 → 20 → 40 → 80 → 160 → 300)
|
|
75
|
+
- Caps at 5 minutes
|
|
76
|
+
- Resets if agent runs for >5 minutes before crashing
|
|
77
|
+
|
|
78
|
+
## Graceful Shutdown
|
|
79
|
+
|
|
80
|
+
Agents can check for stop signals:
|
|
81
|
+
```bash
|
|
82
|
+
# In bash
|
|
83
|
+
if [ -f ~/.agentchat/agents/YOUR_NAME/stop ]; then
|
|
84
|
+
echo "Shutdown requested, saving state..."
|
|
85
|
+
exit 0
|
|
86
|
+
fi
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Multiple Agents
|
|
90
|
+
|
|
91
|
+
You can run multiple specialized agents:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
agentctl start monitor "monitor agentchat, moderate, respond to questions"
|
|
95
|
+
agentctl start social "manage moltx/moltbook, post updates, engage with mentions"
|
|
96
|
+
agentctl start builder "work on assigned tasks from #tasks channel"
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Viewing All State
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
# Status of all agents
|
|
103
|
+
agentctl status
|
|
104
|
+
|
|
105
|
+
# List registered agents
|
|
106
|
+
agentctl list
|
|
107
|
+
|
|
108
|
+
# View specific agent's saved context
|
|
109
|
+
agentctl context monitor
|
|
110
|
+
```
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Agent Supervisor - manages Claude agent lifecycle with automatic restart and backoff
|
|
3
|
+
# Usage: ./agent-supervisor.sh <agent-name> <mission>
|
|
4
|
+
|
|
5
|
+
set -e
|
|
6
|
+
|
|
7
|
+
AGENT_NAME="${1:-default}"
|
|
8
|
+
MISSION="${2:-monitor agentchat and respond to messages}"
|
|
9
|
+
STATE_DIR="$HOME/.agentchat/agents/$AGENT_NAME"
|
|
10
|
+
LOG_FILE="$STATE_DIR/supervisor.log"
|
|
11
|
+
PID_FILE="$STATE_DIR/supervisor.pid"
|
|
12
|
+
STOP_FILE="$STATE_DIR/stop"
|
|
13
|
+
STATE_FILE="$STATE_DIR/state.json"
|
|
14
|
+
|
|
15
|
+
# Backoff settings
|
|
16
|
+
MIN_BACKOFF=5
|
|
17
|
+
MAX_BACKOFF=300
|
|
18
|
+
BACKOFF_MULTIPLIER=2
|
|
19
|
+
|
|
20
|
+
mkdir -p "$STATE_DIR"
|
|
21
|
+
|
|
22
|
+
log() {
|
|
23
|
+
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
save_state() {
|
|
27
|
+
local status="$1"
|
|
28
|
+
local error="$2"
|
|
29
|
+
cat > "$STATE_FILE" << EOF
|
|
30
|
+
{
|
|
31
|
+
"agent_name": "$AGENT_NAME",
|
|
32
|
+
"mission": "$MISSION",
|
|
33
|
+
"status": "$status",
|
|
34
|
+
"last_error": "$error",
|
|
35
|
+
"restart_count": $RESTART_COUNT,
|
|
36
|
+
"started_at": "$STARTED_AT",
|
|
37
|
+
"updated_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
|
38
|
+
"pid": $$
|
|
39
|
+
}
|
|
40
|
+
EOF
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
cleanup() {
|
|
44
|
+
log "Supervisor shutting down"
|
|
45
|
+
save_state "stopped" ""
|
|
46
|
+
rm -f "$PID_FILE"
|
|
47
|
+
exit 0
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
trap cleanup SIGINT SIGTERM
|
|
51
|
+
|
|
52
|
+
# Check if already running
|
|
53
|
+
if [ -f "$PID_FILE" ]; then
|
|
54
|
+
OLD_PID=$(cat "$PID_FILE")
|
|
55
|
+
if ps -p "$OLD_PID" > /dev/null 2>&1; then
|
|
56
|
+
log "Supervisor already running (PID $OLD_PID)"
|
|
57
|
+
exit 1
|
|
58
|
+
fi
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
echo $$ > "$PID_FILE"
|
|
62
|
+
rm -f "$STOP_FILE"
|
|
63
|
+
|
|
64
|
+
RESTART_COUNT=0
|
|
65
|
+
BACKOFF=$MIN_BACKOFF
|
|
66
|
+
STARTED_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
67
|
+
|
|
68
|
+
log "Starting supervisor for agent '$AGENT_NAME'"
|
|
69
|
+
log "Mission: $MISSION"
|
|
70
|
+
save_state "starting" ""
|
|
71
|
+
|
|
72
|
+
while true; do
|
|
73
|
+
# Check for stop signal
|
|
74
|
+
if [ -f "$STOP_FILE" ]; then
|
|
75
|
+
log "Stop file detected, shutting down"
|
|
76
|
+
rm -f "$STOP_FILE"
|
|
77
|
+
cleanup
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
log "Starting agent (attempt $((RESTART_COUNT + 1)), backoff ${BACKOFF}s)"
|
|
81
|
+
save_state "running" ""
|
|
82
|
+
|
|
83
|
+
# Build the resume prompt with state context
|
|
84
|
+
RESUME_PROMPT="You are agent '$AGENT_NAME'. Your mission: $MISSION
|
|
85
|
+
|
|
86
|
+
IMPORTANT: You are being restarted by a supervisor. Check your state file at:
|
|
87
|
+
$STATE_FILE
|
|
88
|
+
|
|
89
|
+
Read ~/.agentchat/agents/$AGENT_NAME/context.md for any saved context from your previous run.
|
|
90
|
+
Before doing significant work, save your current task to context.md so you can resume if interrupted.
|
|
91
|
+
|
|
92
|
+
On quota errors or before shutdown, write your current state to context.md.
|
|
93
|
+
|
|
94
|
+
Begin your mission now."
|
|
95
|
+
|
|
96
|
+
# Run claude with the mission
|
|
97
|
+
START_TIME=$(date +%s)
|
|
98
|
+
|
|
99
|
+
if claude -p "$RESUME_PROMPT" 2>> "$LOG_FILE"; then
|
|
100
|
+
# Clean exit
|
|
101
|
+
log "Agent exited cleanly"
|
|
102
|
+
BACKOFF=$MIN_BACKOFF
|
|
103
|
+
else
|
|
104
|
+
EXIT_CODE=$?
|
|
105
|
+
END_TIME=$(date +%s)
|
|
106
|
+
DURATION=$((END_TIME - START_TIME))
|
|
107
|
+
|
|
108
|
+
log "Agent crashed (exit code $EXIT_CODE, ran for ${DURATION}s)"
|
|
109
|
+
save_state "crashed" "exit_code=$EXIT_CODE"
|
|
110
|
+
|
|
111
|
+
# If it ran for more than 5 minutes, reset backoff
|
|
112
|
+
if [ $DURATION -gt 300 ]; then
|
|
113
|
+
BACKOFF=$MIN_BACKOFF
|
|
114
|
+
log "Ran long enough, resetting backoff"
|
|
115
|
+
else
|
|
116
|
+
# Exponential backoff
|
|
117
|
+
BACKOFF=$((BACKOFF * BACKOFF_MULTIPLIER))
|
|
118
|
+
if [ $BACKOFF -gt $MAX_BACKOFF ]; then
|
|
119
|
+
BACKOFF=$MAX_BACKOFF
|
|
120
|
+
fi
|
|
121
|
+
fi
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
RESTART_COUNT=$((RESTART_COUNT + 1))
|
|
125
|
+
|
|
126
|
+
# Check for stop signal before sleeping
|
|
127
|
+
if [ -f "$STOP_FILE" ]; then
|
|
128
|
+
log "Stop file detected, shutting down"
|
|
129
|
+
rm -f "$STOP_FILE"
|
|
130
|
+
cleanup
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
log "Waiting ${BACKOFF}s before restart..."
|
|
134
|
+
sleep $BACKOFF
|
|
135
|
+
done
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# agentctl - manage supervised Claude agents
|
|
3
|
+
# Usage: agentctl <command> [agent-name] [options]
|
|
4
|
+
|
|
5
|
+
AGENTS_DIR="$HOME/.agentchat/agents"
|
|
6
|
+
SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "$0" 2>/dev/null || echo "$0")")" && pwd)"
|
|
7
|
+
SUPERVISOR_SCRIPT="$SCRIPT_DIR/agent-supervisor.sh"
|
|
8
|
+
|
|
9
|
+
usage() {
|
|
10
|
+
cat << EOF
|
|
11
|
+
Usage: agentctl <command> [agent-name] [options]
|
|
12
|
+
|
|
13
|
+
Commands:
|
|
14
|
+
start <name> <mission> Start a new supervised agent
|
|
15
|
+
stop <name> Stop an agent gracefully
|
|
16
|
+
kill <name> Force kill an agent
|
|
17
|
+
restart <name> Restart an agent
|
|
18
|
+
status [name] Show agent status (all if no name)
|
|
19
|
+
logs <name> [lines] Show agent logs
|
|
20
|
+
list List all agents
|
|
21
|
+
context <name> Show agent's saved context
|
|
22
|
+
stopall Stop all agents
|
|
23
|
+
|
|
24
|
+
Examples:
|
|
25
|
+
agentctl start monitor "monitor agentchat #general and moderate"
|
|
26
|
+
agentctl start social "manage moltx and moltbook social media"
|
|
27
|
+
agentctl stop monitor
|
|
28
|
+
agentctl status
|
|
29
|
+
EOF
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
start_agent() {
|
|
33
|
+
local name="$1"
|
|
34
|
+
local mission="$2"
|
|
35
|
+
|
|
36
|
+
if [ -z "$name" ] || [ -z "$mission" ]; then
|
|
37
|
+
echo "Usage: agentctl start <name> <mission>"
|
|
38
|
+
exit 1
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
local state_dir="$AGENTS_DIR/$name"
|
|
42
|
+
mkdir -p "$state_dir"
|
|
43
|
+
|
|
44
|
+
# Check if already running
|
|
45
|
+
if [ -f "$state_dir/supervisor.pid" ]; then
|
|
46
|
+
local pid=$(cat "$state_dir/supervisor.pid")
|
|
47
|
+
if ps -p "$pid" > /dev/null 2>&1; then
|
|
48
|
+
echo "Agent '$name' already running (PID $pid)"
|
|
49
|
+
exit 1
|
|
50
|
+
fi
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
# Save mission for restarts
|
|
54
|
+
echo "$mission" > "$state_dir/mission.txt"
|
|
55
|
+
|
|
56
|
+
# Initialize context file
|
|
57
|
+
if [ ! -f "$state_dir/context.md" ]; then
|
|
58
|
+
cat > "$state_dir/context.md" << EOF
|
|
59
|
+
# Agent: $name
|
|
60
|
+
## Mission
|
|
61
|
+
$mission
|
|
62
|
+
|
|
63
|
+
## Current State
|
|
64
|
+
Starting fresh.
|
|
65
|
+
|
|
66
|
+
## Notes
|
|
67
|
+
(Save important context here before shutdown)
|
|
68
|
+
EOF
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
echo "Starting agent '$name'..."
|
|
72
|
+
nohup "$SUPERVISOR_SCRIPT" "$name" "$mission" > /dev/null 2>&1 &
|
|
73
|
+
echo "Agent '$name' started (supervisor PID $!)"
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
stop_agent() {
|
|
77
|
+
local name="$1"
|
|
78
|
+
local state_dir="$AGENTS_DIR/$name"
|
|
79
|
+
|
|
80
|
+
if [ ! -d "$state_dir" ]; then
|
|
81
|
+
echo "Agent '$name' not found"
|
|
82
|
+
exit 1
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
# Create stop file for graceful shutdown
|
|
86
|
+
touch "$state_dir/stop"
|
|
87
|
+
echo "Stop signal sent to '$name'"
|
|
88
|
+
|
|
89
|
+
# Wait a moment then check
|
|
90
|
+
sleep 2
|
|
91
|
+
if [ -f "$state_dir/supervisor.pid" ]; then
|
|
92
|
+
local pid=$(cat "$state_dir/supervisor.pid")
|
|
93
|
+
if ps -p "$pid" > /dev/null 2>&1; then
|
|
94
|
+
echo "Agent still running, waiting..."
|
|
95
|
+
sleep 5
|
|
96
|
+
if ps -p "$pid" > /dev/null 2>&1; then
|
|
97
|
+
echo "Agent didn't stop gracefully, use 'agentctl kill $name'"
|
|
98
|
+
fi
|
|
99
|
+
else
|
|
100
|
+
echo "Agent '$name' stopped"
|
|
101
|
+
fi
|
|
102
|
+
fi
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
kill_agent() {
|
|
106
|
+
local name="$1"
|
|
107
|
+
local state_dir="$AGENTS_DIR/$name"
|
|
108
|
+
|
|
109
|
+
if [ ! -d "$state_dir" ]; then
|
|
110
|
+
echo "Agent '$name' not found"
|
|
111
|
+
exit 1
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
if [ -f "$state_dir/supervisor.pid" ]; then
|
|
115
|
+
local pid=$(cat "$state_dir/supervisor.pid")
|
|
116
|
+
if ps -p "$pid" > /dev/null 2>&1; then
|
|
117
|
+
# Kill the supervisor and its children
|
|
118
|
+
pkill -P "$pid" 2>/dev/null
|
|
119
|
+
kill "$pid" 2>/dev/null
|
|
120
|
+
rm -f "$state_dir/supervisor.pid"
|
|
121
|
+
echo "Agent '$name' killed"
|
|
122
|
+
else
|
|
123
|
+
echo "Agent '$name' not running"
|
|
124
|
+
rm -f "$state_dir/supervisor.pid"
|
|
125
|
+
fi
|
|
126
|
+
else
|
|
127
|
+
echo "No PID file for '$name'"
|
|
128
|
+
fi
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
show_status() {
|
|
132
|
+
local name="$1"
|
|
133
|
+
|
|
134
|
+
if [ -n "$name" ]; then
|
|
135
|
+
local state_dir="$AGENTS_DIR/$name"
|
|
136
|
+
if [ -f "$state_dir/state.json" ]; then
|
|
137
|
+
cat "$state_dir/state.json" | python3 -m json.tool 2>/dev/null || cat "$state_dir/state.json"
|
|
138
|
+
else
|
|
139
|
+
echo "No state file for '$name'"
|
|
140
|
+
fi
|
|
141
|
+
else
|
|
142
|
+
echo "=== Agent Status ==="
|
|
143
|
+
for dir in "$AGENTS_DIR"/*/; do
|
|
144
|
+
if [ -d "$dir" ]; then
|
|
145
|
+
local agent=$(basename "$dir")
|
|
146
|
+
local status="unknown"
|
|
147
|
+
local pid=""
|
|
148
|
+
|
|
149
|
+
if [ -f "$dir/state.json" ]; then
|
|
150
|
+
status=$(python3 -c "import json; print(json.load(open('$dir/state.json')).get('status', 'unknown'))" 2>/dev/null || echo "unknown")
|
|
151
|
+
fi
|
|
152
|
+
|
|
153
|
+
if [ -f "$dir/supervisor.pid" ]; then
|
|
154
|
+
pid=$(cat "$dir/supervisor.pid")
|
|
155
|
+
if ! ps -p "$pid" > /dev/null 2>&1; then
|
|
156
|
+
status="dead"
|
|
157
|
+
pid=""
|
|
158
|
+
fi
|
|
159
|
+
fi
|
|
160
|
+
|
|
161
|
+
printf "%-15s %-10s %s\n" "$agent" "$status" "${pid:+PID $pid}"
|
|
162
|
+
fi
|
|
163
|
+
done
|
|
164
|
+
fi
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
show_logs() {
|
|
168
|
+
local name="$1"
|
|
169
|
+
local lines="${2:-50}"
|
|
170
|
+
local log_file="$AGENTS_DIR/$name/supervisor.log"
|
|
171
|
+
|
|
172
|
+
if [ -f "$log_file" ]; then
|
|
173
|
+
tail -n "$lines" "$log_file"
|
|
174
|
+
else
|
|
175
|
+
echo "No logs for '$name'"
|
|
176
|
+
fi
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
list_agents() {
|
|
180
|
+
echo "=== Registered Agents ==="
|
|
181
|
+
for dir in "$AGENTS_DIR"/*/; do
|
|
182
|
+
if [ -d "$dir" ]; then
|
|
183
|
+
local agent=$(basename "$dir")
|
|
184
|
+
local mission=""
|
|
185
|
+
if [ -f "$dir/mission.txt" ]; then
|
|
186
|
+
mission=$(cat "$dir/mission.txt")
|
|
187
|
+
fi
|
|
188
|
+
echo "$agent: $mission"
|
|
189
|
+
fi
|
|
190
|
+
done
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
show_context() {
|
|
194
|
+
local name="$1"
|
|
195
|
+
local context_file="$AGENTS_DIR/$name/context.md"
|
|
196
|
+
|
|
197
|
+
if [ -f "$context_file" ]; then
|
|
198
|
+
cat "$context_file"
|
|
199
|
+
else
|
|
200
|
+
echo "No context file for '$name'"
|
|
201
|
+
fi
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
stop_all() {
|
|
205
|
+
echo "Stopping all agents..."
|
|
206
|
+
for dir in "$AGENTS_DIR"/*/; do
|
|
207
|
+
if [ -d "$dir" ]; then
|
|
208
|
+
local agent=$(basename "$dir")
|
|
209
|
+
touch "$dir/stop"
|
|
210
|
+
echo "Stop signal sent to '$agent'"
|
|
211
|
+
fi
|
|
212
|
+
done
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
# Main
|
|
216
|
+
case "$1" in
|
|
217
|
+
start)
|
|
218
|
+
start_agent "$2" "$3"
|
|
219
|
+
;;
|
|
220
|
+
stop)
|
|
221
|
+
stop_agent "$2"
|
|
222
|
+
;;
|
|
223
|
+
kill)
|
|
224
|
+
kill_agent "$2"
|
|
225
|
+
;;
|
|
226
|
+
restart)
|
|
227
|
+
stop_agent "$2"
|
|
228
|
+
sleep 3
|
|
229
|
+
mission=$(cat "$AGENTS_DIR/$2/mission.txt" 2>/dev/null)
|
|
230
|
+
start_agent "$2" "$mission"
|
|
231
|
+
;;
|
|
232
|
+
status)
|
|
233
|
+
show_status "$2"
|
|
234
|
+
;;
|
|
235
|
+
logs)
|
|
236
|
+
show_logs "$2" "$3"
|
|
237
|
+
;;
|
|
238
|
+
list)
|
|
239
|
+
list_agents
|
|
240
|
+
;;
|
|
241
|
+
context)
|
|
242
|
+
show_context "$2"
|
|
243
|
+
;;
|
|
244
|
+
stopall)
|
|
245
|
+
stop_all
|
|
246
|
+
;;
|
|
247
|
+
*)
|
|
248
|
+
usage
|
|
249
|
+
;;
|
|
250
|
+
esac
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Kill switch checker - stops all agents if kill file exists
|
|
3
|
+
# Kill file locations (check any of these):
|
|
4
|
+
# 1. iCloud: ~/Library/Mobile Documents/com~apple~CloudDocs/KILL_AGENTS
|
|
5
|
+
# 2. Local: ~/.agentchat/KILL
|
|
6
|
+
# 3. Dropbox: ~/Dropbox/KILL_AGENTS (if exists)
|
|
7
|
+
|
|
8
|
+
ICLOUD_KILL="$HOME/Library/Mobile Documents/com~apple~CloudDocs/KILL_AGENTS"
|
|
9
|
+
LOCAL_KILL="$HOME/.agentchat/KILL"
|
|
10
|
+
DROPBOX_KILL="$HOME/Dropbox/KILL_AGENTS"
|
|
11
|
+
|
|
12
|
+
check_kill() {
|
|
13
|
+
if [ -f "$ICLOUD_KILL" ] || [ -f "$LOCAL_KILL" ] || [ -f "$DROPBOX_KILL" ]; then
|
|
14
|
+
return 0 # Kill signal found
|
|
15
|
+
fi
|
|
16
|
+
return 1 # No kill signal
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if check_kill; then
|
|
20
|
+
echo "KILL SIGNAL DETECTED"
|
|
21
|
+
echo "Stopping all agents..."
|
|
22
|
+
|
|
23
|
+
# Stop all supervised agents
|
|
24
|
+
"$HOME/bin/agentctl" stopall 2>/dev/null
|
|
25
|
+
|
|
26
|
+
# Kill any claude processes
|
|
27
|
+
pkill -f "claude" 2>/dev/null
|
|
28
|
+
|
|
29
|
+
# Clean up kill files
|
|
30
|
+
rm -f "$ICLOUD_KILL" "$LOCAL_KILL" "$DROPBOX_KILL" 2>/dev/null
|
|
31
|
+
|
|
32
|
+
echo "All agents terminated."
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
exit 0
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Send notification to phone via ntfy.sh (free, no signup needed)
|
|
3
|
+
# Usage: notify.sh "title" "message" [priority]
|
|
4
|
+
# Priority: 1=min, 2=low, 3=default, 4=high, 5=urgent
|
|
5
|
+
|
|
6
|
+
TOPIC="agentchat-james-$(whoami | md5sum | cut -c1-8)" # Unique topic
|
|
7
|
+
TITLE="${1:-Agent Alert}"
|
|
8
|
+
MESSAGE="${2:-Something happened}"
|
|
9
|
+
PRIORITY="${3:-3}"
|
|
10
|
+
|
|
11
|
+
# Send via ntfy.sh
|
|
12
|
+
curl -s \
|
|
13
|
+
-H "Title: $TITLE" \
|
|
14
|
+
-H "Priority: $PRIORITY" \
|
|
15
|
+
-H "Tags: robot" \
|
|
16
|
+
-d "$MESSAGE" \
|
|
17
|
+
"https://ntfy.sh/$TOPIC" > /dev/null
|
|
18
|
+
|
|
19
|
+
echo "Notification sent to ntfy.sh/$TOPIC"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tjamescouch/agentchat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.1",
|
|
4
4
|
"description": "Real-time IRC-like communication protocol for AI agents",
|
|
5
5
|
"main": "lib/client.js",
|
|
6
6
|
"files": [
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
},
|
|
15
15
|
"scripts": {
|
|
16
16
|
"start": "node bin/agentchat.js serve",
|
|
17
|
-
"test": "node --test test
|
|
17
|
+
"test": "node --test 'test/*.test.js'",
|
|
18
18
|
"test:integration": "node --test test/*.integration.test.js",
|
|
19
19
|
"test:all": "node --test test/*.test.js"
|
|
20
20
|
},
|