@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 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
- this._identity = await Identity.load(this.identityPath);
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 from ${this.identityPath}: ${err.message}`);
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
@@ -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.19.1",
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/identity.test.js test/deploy.test.js test/daemon.test.js test/receipts.test.js test/reputation.test.js test/jitter.test.js",
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
  },