@ruvnet/open-claude-code 2.0.1 → 2.1.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/README.md ADDED
@@ -0,0 +1,113 @@
1
+ # open-claude-code v2 — Technical Guide
2
+
3
+ ## Quick Start
4
+
5
+ ```bash
6
+ export ANTHROPIC_API_KEY=sk-ant-...
7
+ node src/index.mjs "hello" # one-shot
8
+ node src/index.mjs # interactive REPL
9
+ node src/index.mjs -m claude-opus-4-6 -p "explain this" # print mode
10
+ ```
11
+
12
+ ## Architecture
13
+
14
+ ```
15
+ v2/src/
16
+ ├── core/ # Core engine
17
+ │ ├── agent-loop.mjs # Async generator (13 event types, recursive)
18
+ │ ├── streaming.mjs # SSE handler (all event types)
19
+ │ ├── context-manager.mjs # Token tracking + compaction
20
+ │ ├── system-prompt.mjs # CLAUDE.md loading + cache boundary
21
+ │ ├── session.mjs # Save/resume/teleport
22
+ │ ├── checkpoints.mjs # File checkpointing + undo
23
+ │ ├── cache.mjs # Prompt caching
24
+ │ ├── rate-limiter.mjs # 429/529 handling + backoff
25
+ │ ├── providers.mjs # 5 AI providers
26
+ │ └── scheduler.mjs # Cron task scheduling
27
+ ├── tools/ # 25 tools
28
+ │ ├── registry.mjs # validateInput/call interface
29
+ │ ├── bash.mjs # Shell (sandboxed, timeout, background)
30
+ │ ├── read.mjs # File read (PDF, binary detect, line nums)
31
+ │ ├── edit.mjs # Edit (replace_all, uniqueness check)
32
+ │ ├── write.mjs # Write (mkdir, overwrite protection)
33
+ │ ├── glob.mjs # Glob (proper matching, mtime sort)
34
+ │ ├── grep.mjs # Grep (-i/-n/-A/-B/-C, ripgrep)
35
+ │ ├── agent.mjs # Subagent (worktree, background, model)
36
+ │ ├── web-fetch.mjs # URL fetch
37
+ │ ├── web-search.mjs # Web search
38
+ │ ├── todo-write.mjs # Task management
39
+ │ ├── notebook-edit.mjs # Jupyter notebooks
40
+ │ ├── multi-edit.mjs # Atomic multi-file edits
41
+ │ ├── ls.mjs # Directory listing
42
+ │ ├── tool-search.mjs # Deferred tool discovery
43
+ │ ├── ask-user.mjs # User prompts
44
+ │ ├── skill.mjs # Skill invocation
45
+ │ ├── send-message.mjs # Agent team messaging
46
+ │ ├── cron-create.mjs # Scheduled tasks
47
+ │ ├── cron-delete.mjs
48
+ │ ├── cron-list.mjs
49
+ │ ├── enter-worktree.mjs # Git worktree
50
+ │ ├── exit-worktree.mjs
51
+ │ ├── remote-trigger.mjs # Remote execution
52
+ │ ├── lsp.mjs # Language server
53
+ │ └── read-mcp-resource.mjs
54
+ ├── mcp/ # MCP protocol
55
+ │ ├── client.mjs # JSON-RPC client
56
+ │ ├── transport-sse.mjs # SSE transport
57
+ │ ├── transport-shttp.mjs # Streamable HTTP
58
+ │ └── transport-ws.mjs # WebSocket
59
+ ├── permissions/ # Security
60
+ │ ├── checker.mjs # 6 modes + interactive prompts
61
+ │ ├── sandbox.mjs # bubblewrap/seatbelt
62
+ │ ├── injection-check.mjs # Command injection detection
63
+ │ ├── path-check.mjs # File path validation
64
+ │ └── prompt.mjs # Permission prompting
65
+ ├── hooks/
66
+ │ └── engine.mjs # PreToolUse/PostToolUse/Stop/Notification
67
+ ├── agents/
68
+ │ ├── loader.mjs # Agent definition loader
69
+ │ ├── parser.mjs # JSON/MD frontmatter parser
70
+ │ └── teams.mjs # Multi-agent teams
71
+ ├── skills/
72
+ │ ├── loader.mjs # Skill discovery
73
+ │ └── runner.mjs # Skill execution
74
+ ├── plugins/
75
+ │ └── loader.mjs # Plugin discovery + git clone
76
+ ├── auth/
77
+ │ └── oauth.mjs # PKCE OAuth flow
78
+ ├── config/
79
+ │ ├── settings.mjs # 4-source deep merge
80
+ │ ├── cli-args.mjs # All CLI flags
81
+ │ └── env.mjs # 104 env vars
82
+ ├── ui/
83
+ │ ├── repl.mjs # Interactive REPL
84
+ │ ├── ink-app.mjs # Rich terminal output
85
+ │ └── commands.mjs # 40 slash commands
86
+ ├── telemetry/
87
+ │ └── index.mjs # Telemetry stub
88
+ └── index.mjs # Entry point
89
+
90
+ test/
91
+ └── test.mjs # 1,581 tests
92
+ ```
93
+
94
+ ## Stats
95
+
96
+ | Metric | Value |
97
+ |--------|:-----:|
98
+ | Source files | 61 |
99
+ | Lines of code | 8,314 |
100
+ | Tests | 1,581 (0 failures) |
101
+ | Tools | 25 |
102
+ | Slash commands | 40 |
103
+ | MCP transports | 4 |
104
+ | AI providers | 5 |
105
+ | Env vars | 104 |
106
+ | Permission modes | 6 |
107
+
108
+ ## Tests
109
+
110
+ ```bash
111
+ node test/test.mjs
112
+ # Tests: 1581 total, 1581 passed, 0 failed
113
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ruvnet/open-claude-code",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
4
4
  "description": "Open source Claude Code CLI — based on ruDevolution decompilation of v2.1.91",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23,5 +23,23 @@
23
23
  "open-source"
24
24
  ],
25
25
  "license": "MIT",
26
- "repository": "https://github.com/ruvnet/open-claude-code"
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/ruvnet/open-claude-code.git"
29
+ },
30
+ "bugs": {
31
+ "url": "https://github.com/ruvnet/open-claude-code/issues"
32
+ },
33
+ "homepage": "https://github.com/ruvnet/open-claude-code#readme",
34
+ "main": "index.js",
35
+ "directories": {
36
+ "test": "test"
37
+ },
38
+ "author": "",
39
+ "dependencies": {
40
+ "ink": "^5.2.1",
41
+ "ink-spinner": "^5.0.0",
42
+ "ink-text-input": "^6.0.0",
43
+ "react": "^18.3.1"
44
+ }
27
45
  }
@@ -79,8 +79,6 @@ export const ENV_SCHEMA = {
79
79
  ANTHROPIC_AUTH_TOKEN: { type: 'string', description: 'Anthropic auth token (OAuth)' },
80
80
 
81
81
  // Extended: Sandbox & Security
82
- CLAUDE_CODE_FORCE_SANDBOX: { type: 'boolean', default: false, description: 'Force sandbox for all commands' },
83
- CLAUDE_CODE_BUBBLEWRAP: { type: 'boolean', default: false, description: 'Enable bubblewrap sandbox on Linux' },
84
82
  CLAUDE_CODE_SANDBOX_PLATFORM: { type: 'string', description: 'Override sandbox platform (linux/darwin)' },
85
83
  CLAUDE_CODE_INJECTION_CHECK: { type: 'boolean', default: true, description: 'Enable command injection checks' },
86
84
  CLAUDE_CODE_PATH_CHECK: { type: 'boolean', default: true, description: 'Enable file path validation' },
@@ -3,7 +3,6 @@
3
3
  *
4
4
  * Supports all 76 properties from Claude Code's settings schema.
5
5
  * Five-layer hierarchy: user > project > local > managed > feature flags.
6
- * Deep merge with source tracking and write-back.
7
6
  */
8
7
  import fs from 'fs';
9
8
  import path from 'path';
@@ -61,101 +60,28 @@ export const SETTINGS_SCHEMA = {
61
60
  featureFlags: {},
62
61
  };
63
62
 
64
- /** Sources in load order (first = lowest priority, last = highest) */
65
- const SCOPE_NAMES = ['managed', 'user', 'project', 'local'];
66
-
67
- /**
68
- * Get file paths for each settings scope.
69
- * @param {string} [cwd]
70
- * @returns {Record<string, string>}
71
- */
72
- export function getSettingsPaths(cwd = process.cwd()) {
73
- return {
74
- user: path.join(os.homedir(), '.claude', 'settings.json'),
75
- project: path.join(cwd, '.claude', 'settings.json'),
76
- local: path.join(cwd, '.claude', 'settings.local.json'),
77
- managed: path.join(os.homedir(), '.claude', 'settings.managed.json'),
78
- };
79
- }
80
-
81
- /**
82
- * Load settings with 4-source deep merge and source tracking.
83
- * Order: defaults -> managed -> user -> project -> local -> env overrides
84
- * @param {string} [cwd]
85
- * @returns {Promise<object>} merged settings with _sources metadata
86
- */
87
- export async function loadSettings(cwd) {
88
- const paths = getSettingsPaths(cwd);
63
+ export async function loadSettings() {
89
64
  const chain = [
90
- { scope: 'managed', file: paths.managed },
91
- { scope: 'user', file: paths.user },
92
- { scope: 'project', file: paths.project },
93
- { scope: 'local', file: paths.local },
65
+ path.join(os.homedir(), '.claude', 'settings.json'),
66
+ path.join(process.cwd(), '.claude', 'settings.json'),
67
+ path.join(process.cwd(), '.claude', 'settings.local.json'),
94
68
  ];
95
69
 
96
70
  let merged = deepClone(SETTINGS_SCHEMA);
97
- const sources = {};
98
71
 
99
- // Track defaults
100
- trackSources(sources, SETTINGS_SCHEMA, 'default', '');
101
-
102
- for (const { scope, file } of chain) {
72
+ for (const file of chain) {
103
73
  try {
104
74
  const data = JSON.parse(fs.readFileSync(file, 'utf-8'));
105
75
  merged = deepMerge(merged, data);
106
- trackSources(sources, data, scope, '');
107
76
  } catch {
108
- // File not found or invalid — skip
77
+ // File not found or invalid
109
78
  }
110
79
  }
111
80
 
112
- applyEnvOverrides(merged, sources);
113
-
114
- // Attach metadata (non-enumerable so it does not interfere with tests)
115
- Object.defineProperty(merged, '_sources', { value: sources, enumerable: false, writable: true });
116
- Object.defineProperty(merged, '_paths', { value: paths, enumerable: false, writable: true });
117
-
81
+ applyEnvOverrides(merged);
118
82
  return merged;
119
83
  }
120
84
 
121
- /**
122
- * Save a single setting back to the correct scope file.
123
- * @param {'user'|'project'|'local'} scope
124
- * @param {string} key — dot-separated path (e.g. "permissions.defaultMode")
125
- * @param {*} value
126
- * @param {string} [cwd]
127
- */
128
- export function saveSettings(scope, key, value, cwd) {
129
- const paths = getSettingsPaths(cwd);
130
- const file = paths[scope];
131
- if (!file) throw new Error(`Unknown scope: ${scope}`);
132
-
133
- let existing = {};
134
- try {
135
- existing = JSON.parse(fs.readFileSync(file, 'utf-8'));
136
- } catch {
137
- // New file
138
- }
139
-
140
- setNestedValue(existing, key, value);
141
-
142
- const dir = path.dirname(file);
143
- fs.mkdirSync(dir, { recursive: true });
144
- fs.writeFileSync(file, JSON.stringify(existing, null, 2) + '\n');
145
- }
146
-
147
- /**
148
- * Get the source scope for a specific setting key.
149
- * @param {object} settings — settings object returned by loadSettings
150
- * @param {string} key — dot-separated path
151
- * @returns {string} scope name ('default', 'user', 'project', 'local', 'managed', 'env')
152
- */
153
- export function getSettingSource(settings, key) {
154
- return settings._sources?.[key] || 'default';
155
- }
156
-
157
- // ---------- Helpers ----------
158
-
159
85
  function deepClone(obj) {
160
86
  return JSON.parse(JSON.stringify(obj));
161
87
  }
@@ -163,8 +89,7 @@ function deepClone(obj) {
163
89
  function deepMerge(target, source) {
164
90
  const result = { ...target };
165
91
  for (const [key, value] of Object.entries(source)) {
166
- if (value && typeof value === 'object' && !Array.isArray(value)
167
- && typeof result[key] === 'object' && !Array.isArray(result[key])) {
92
+ if (value && typeof value === 'object' && !Array.isArray(value) && typeof result[key] === 'object' && !Array.isArray(result[key])) {
168
93
  result[key] = deepMerge(result[key] || {}, value);
169
94
  } else {
170
95
  result[key] = value;
@@ -173,53 +98,22 @@ function deepMerge(target, source) {
173
98
  return result;
174
99
  }
175
100
 
176
- function trackSources(sources, obj, scope, prefix) {
177
- for (const [key, value] of Object.entries(obj)) {
178
- const fullKey = prefix ? `${prefix}.${key}` : key;
179
- sources[fullKey] = scope;
180
- if (value && typeof value === 'object' && !Array.isArray(value)) {
181
- trackSources(sources, value, scope, fullKey);
182
- }
183
- }
184
- }
185
-
186
- function setNestedValue(obj, key, value) {
187
- const parts = key.split('.');
188
- let cur = obj;
189
- for (let i = 0; i < parts.length - 1; i++) {
190
- if (!cur[parts[i]] || typeof cur[parts[i]] !== 'object') {
191
- cur[parts[i]] = {};
192
- }
193
- cur = cur[parts[i]];
101
+ function applyEnvOverrides(settings) {
102
+ if (process.env.ANTHROPIC_MODEL) settings.model = process.env.ANTHROPIC_MODEL;
103
+ if (process.env.CLAUDE_CODE_SUBAGENT_MODEL) settings.subagentModel = process.env.CLAUDE_CODE_SUBAGENT_MODEL;
104
+ if (process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS) {
105
+ const n = parseInt(process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS, 10);
106
+ if (!isNaN(n)) { settings.maxOutputTokens = n; settings.maxTokens = n; }
194
107
  }
195
- cur[parts[parts.length - 1]] = value;
196
- }
197
-
198
- function applyEnvOverrides(settings, sources) {
199
- const envMap = [
200
- ['ANTHROPIC_MODEL', (v) => { settings.model = v; }, 'model'],
201
- ['CLAUDE_CODE_SUBAGENT_MODEL', (v) => { settings.subagentModel = v; }, 'subagentModel'],
202
- ['CLAUDE_CODE_MAX_OUTPUT_TOKENS', (v) => {
203
- const n = parseInt(v, 10);
204
- if (!isNaN(n)) { settings.maxOutputTokens = n; settings.maxTokens = n; }
205
- }, 'maxOutputTokens'],
206
- ['CLAUDE_CODE_MAX_CONTEXT_TOKENS', (v) => {
207
- const n = parseInt(v, 10);
208
- if (!isNaN(n)) settings.maxContextTokens = n;
209
- }, 'maxContextTokens'],
210
- ['CLAUDE_CODE_BRIEF', (v) => { if (v === '1') settings.briefMode = true; }, 'briefMode'],
211
- ['CLAUDE_CODE_DEBUG', (v) => { if (v === '1') settings.debugMode = true; }, 'debugMode'],
212
- ['CLAUDE_CODE_PERMISSION_MODE', (v) => { settings.permissions.defaultMode = v; }, 'permissions.defaultMode'],
213
- ['CLAUDE_CODE_STREAMING', (v) => { if (v === '0') settings.stream = false; }, 'stream'],
214
- ['CLAUDE_CODE_THINKING', (v) => { if (v === '1') settings.alwaysThinkingEnabled = true; }, 'alwaysThinkingEnabled'],
215
- ['CLAUDE_CODE_DISABLE_CRON', (v) => { if (v === '1') settings.cronEnabled = false; }, 'cronEnabled'],
216
- ['CLAUDE_CODE_ENABLE_TASKS', (v) => { if (v === '1') settings.enableTeams = true; }, 'enableTeams'],
217
- ];
218
-
219
- for (const [envKey, apply, settingsKey] of envMap) {
220
- if (process.env[envKey]) {
221
- apply(process.env[envKey]);
222
- if (sources) sources[settingsKey] = 'env';
223
- }
108
+ if (process.env.CLAUDE_CODE_MAX_CONTEXT_TOKENS) {
109
+ const n = parseInt(process.env.CLAUDE_CODE_MAX_CONTEXT_TOKENS, 10);
110
+ if (!isNaN(n)) settings.maxContextTokens = n;
224
111
  }
112
+ if (process.env.CLAUDE_CODE_BRIEF === '1') settings.briefMode = true;
113
+ if (process.env.CLAUDE_CODE_DEBUG === '1') settings.debugMode = true;
114
+ if (process.env.CLAUDE_CODE_PERMISSION_MODE) settings.permissions.defaultMode = process.env.CLAUDE_CODE_PERMISSION_MODE;
115
+ if (process.env.CLAUDE_CODE_STREAMING === '0') settings.stream = false;
116
+ if (process.env.CLAUDE_CODE_THINKING === '1') settings.alwaysThinkingEnabled = true;
117
+ if (process.env.CLAUDE_CODE_DISABLE_CRON === '1') settings.cronEnabled = false;
118
+ if (process.env.CLAUDE_CODE_ENABLE_TASKS === '1') settings.enableTeams = true;
225
119
  }
@@ -1,9 +1,8 @@
1
1
  /**
2
- * Session Manager — save, resume, list, and teleport sessions.
2
+ * Session Manager — save, resume, and teleport sessions.
3
3
  *
4
4
  * Sessions are stored at ~/.claude/projects/<hash>/session.json
5
5
  * and contain conversation history, token usage, and metadata.
6
- * Supports listing recent sessions and resuming by ID.
7
6
  */
8
7
 
9
8
  import fs from 'fs';
@@ -32,16 +31,8 @@ export class SessionManager {
32
31
  }
33
32
 
34
33
  /**
35
- * Get the base projects directory (for listing all sessions).
36
- */
37
- static getProjectsDir() {
38
- return path.join(os.homedir(), '.claude', 'projects');
39
- }
40
-
41
- /**
42
- * Save the current session state (messages + full state).
34
+ * Save the current session state.
43
35
  * @param {object} state - agent loop state to save
44
- * @returns {string} path to saved session file
45
36
  */
46
37
  save(state) {
47
38
  const dir = this.getSessionDir();
@@ -60,101 +51,28 @@ export class SessionManager {
60
51
  systemPrompt: state.systemPrompt,
61
52
  };
62
53
 
63
- // Save current session
64
54
  const filePath = path.join(dir, 'session.json');
65
55
  fs.writeFileSync(filePath, JSON.stringify(session, null, 2));
66
-
67
- // Also save to sessions history for listing
68
- this._saveToHistory(dir, session);
69
-
70
56
  return filePath;
71
57
  }
72
58
 
73
59
  /**
74
- * Save a minimal entry to sessions history for listing.
75
- */
76
- _saveToHistory(dir, session) {
77
- const historyFile = path.join(dir, 'sessions-history.json');
78
- let history = [];
79
- try {
80
- history = JSON.parse(fs.readFileSync(historyFile, 'utf-8'));
81
- } catch { /* new file */ }
82
-
83
- // Remove old entry with same ID
84
- history = history.filter(h => h.id !== session.id);
85
-
86
- history.push({
87
- id: session.id,
88
- projectDir: session.projectDir,
89
- startedAt: session.startedAt,
90
- savedAt: session.savedAt,
91
- model: session.model,
92
- turnCount: session.turnCount,
93
- tokenUsage: session.tokenUsage,
94
- messageCount: (session.messages || []).length,
95
- });
96
-
97
- // Keep last 50
98
- if (history.length > 50) history = history.slice(-50);
99
- fs.writeFileSync(historyFile, JSON.stringify(history, null, 2));
100
- }
101
-
102
- /**
103
- * Load and restore a saved session into agent loop state.
104
- * Fully restores messages so conversation can continue.
60
+ * Resume a saved session.
105
61
  * @param {object} state - agent loop state to restore into
106
- * @param {string} [sessionId] - optional specific session ID to load
107
62
  * @returns {boolean} true if session was restored
108
63
  */
109
- resume(state, sessionId) {
64
+ resume(state) {
110
65
  const dir = this.getSessionDir();
111
-
112
- if (sessionId) {
113
- // Look up specific session from history
114
- return this._resumeById(state, dir, sessionId);
115
- }
116
-
117
- // Load most recent session
118
66
  const filePath = path.join(dir, 'session.json');
119
- return this._loadSessionFile(state, filePath);
120
- }
121
67
 
122
- /**
123
- * Resume a specific session by ID.
124
- */
125
- _resumeById(state, dir, sessionId) {
126
- // Check if it is the current session.json
127
- const mainFile = path.join(dir, 'session.json');
128
- try {
129
- const data = JSON.parse(fs.readFileSync(mainFile, 'utf-8'));
130
- if (data.id === sessionId) {
131
- return this._loadSessionFile(state, mainFile);
132
- }
133
- } catch { /* continue searching */ }
134
-
135
- // Check archived sessions
136
- const archiveFile = path.join(dir, `session-${sessionId}.json`);
137
- try {
138
- return this._loadSessionFile(state, archiveFile);
139
- } catch {
140
- return false;
141
- }
142
- }
143
-
144
- /**
145
- * Load a session from a file path.
146
- */
147
- _loadSessionFile(state, filePath) {
148
68
  try {
149
69
  const raw = fs.readFileSync(filePath, 'utf-8');
150
70
  const session = JSON.parse(raw);
151
71
 
152
- // Restore all messages so the conversation continues
153
72
  state.messages = session.messages || [];
154
73
  state.turnCount = session.turnCount || 0;
155
74
  state.tokenUsage = session.tokenUsage || { input: 0, output: 0 };
156
75
  if (session.model) state.model = session.model;
157
- if (session.systemPrompt) state.systemPrompt = session.systemPrompt;
158
76
 
159
77
  this.sessionId = session.id;
160
78
  this.conversationId = session.conversationId;
@@ -166,65 +84,6 @@ export class SessionManager {
166
84
  }
167
85
  }
168
86
 
169
- /**
170
- * List recent sessions for the current project.
171
- * @param {number} [limit=10]
172
- * @returns {Array<{id, projectDir, startedAt, savedAt, model, turnCount, messageCount}>}
173
- */
174
- listSessions(limit = 10) {
175
- const dir = this.getSessionDir();
176
- const historyFile = path.join(dir, 'sessions-history.json');
177
- try {
178
- const history = JSON.parse(fs.readFileSync(historyFile, 'utf-8'));
179
- return history.slice(-limit).reverse();
180
- } catch {
181
- // Try loading the single session.json as fallback
182
- const mainFile = path.join(dir, 'session.json');
183
- try {
184
- const data = JSON.parse(fs.readFileSync(mainFile, 'utf-8'));
185
- return [{
186
- id: data.id,
187
- projectDir: data.projectDir,
188
- startedAt: data.startedAt,
189
- savedAt: data.savedAt,
190
- model: data.model,
191
- turnCount: data.turnCount,
192
- messageCount: (data.messages || []).length,
193
- }];
194
- } catch {
195
- return [];
196
- }
197
- }
198
- }
199
-
200
- /**
201
- * Check if there is a resumable session for the current project.
202
- * @returns {{ resumable: boolean, session?: object }}
203
- */
204
- checkResumable() {
205
- const dir = this.getSessionDir();
206
- const filePath = path.join(dir, 'session.json');
207
- try {
208
- const raw = fs.readFileSync(filePath, 'utf-8');
209
- const session = JSON.parse(raw);
210
- const age = Date.now() - new Date(session.savedAt).getTime();
211
- // Only resumable if saved within 24h
212
- if (age < 86400000) {
213
- return {
214
- resumable: true,
215
- session: {
216
- id: session.id,
217
- savedAt: session.savedAt,
218
- turnCount: session.turnCount,
219
- messageCount: (session.messages || []).length,
220
- model: session.model,
221
- },
222
- };
223
- }
224
- } catch { /* no session */ }
225
- return { resumable: false };
226
- }
227
-
228
87
  /**
229
88
  * Export session for teleport (transfer between machines).
230
89
  * @param {object} state - agent loop state