@openagents-org/agent-launcher 0.2.112 → 0.2.114

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openagents-org/agent-launcher",
3
- "version": "0.2.112",
3
+ "version": "0.2.114",
4
4
  "description": "OpenAgents Launcher — install, configure, and run AI coding agents from your terminal",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -53,7 +53,7 @@ class BaseAdapter {
53
53
  this._channelQueues = {};
54
54
  this._log = (msg) => {
55
55
  const ts = new Date().toISOString();
56
- console.log(`${ts} INFO adapter: ${msg}`);
56
+ console.log(`${ts} INFO adapter [${this.agentName}]: ${msg}`);
57
57
  };
58
58
  }
59
59
 
@@ -109,20 +109,15 @@ class BaseAdapter {
109
109
  // ------------------------------------------------------------------
110
110
 
111
111
  async _skipExistingEvents() {
112
- try {
113
- while (true) {
114
- const { cursor } = await this.client.pollPending(
115
- this.workspaceId, this.agentName, this.token,
116
- { after: this._lastEventId, limit: 200 }
117
- );
118
- if (!cursor || cursor === this._lastEventId) break;
119
- this._lastEventId = cursor;
120
- }
121
- if (this._lastEventId) {
122
- this._log(`Skipped existing events, cursor at ${this._lastEventId}`);
123
- }
124
- } catch (e) {
125
- this._log(`Failed to skip existing events: ${e.message}`);
112
+ // Jump straight to the head with one server call. Pagination from the
113
+ // start was slow and brittle: on a busy workspace it could take many
114
+ // minutes to chew through historical events 200 at a time, leaving the
115
+ // agent silently behind, and a transient mid-paginate empty response
116
+ // (e.g. shared-cache race) would strand the cursor at a non-head id.
117
+ const head = await this.client.getHeadEventId(this.workspaceId, this.token);
118
+ if (head) {
119
+ this._lastEventId = head;
120
+ this._log(`Skipped existing events, cursor at ${head}`);
126
121
  }
127
122
  }
128
123
 
@@ -390,10 +390,19 @@ class ClaudeAdapter extends BaseAdapter {
390
390
  let mcpConfigFile = null;
391
391
  let cmd;
392
392
 
393
- // Clean env
393
+ // Clean env: strip every CLAUDE_* / AI_AGENT variable inherited from a
394
+ // parent Claude Code (or Claude Agent SDK) process. If we don't, the
395
+ // spawned `claude` thinks it's running under an SDK harness and picks
396
+ // an org-scoped auth path that returns 403 "Account is no longer a
397
+ // member of the organization" even when the user is logged in fine via
398
+ // `claude login`. We let the child rediscover auth from
399
+ // ~/.claude/.credentials.json (or ANTHROPIC_API_KEY if set).
394
400
  const cleanEnv = { ...(this.agentEnv || process.env) };
395
- delete cleanEnv.CLAUDECODE;
396
- delete cleanEnv.CLAUDE_CODE_SESSION;
401
+ for (const k of Object.keys(cleanEnv)) {
402
+ if (k.startsWith('CLAUDE_') || k === 'CLAUDECODE' || k === 'AI_AGENT') {
403
+ delete cleanEnv[k];
404
+ }
405
+ }
397
406
 
398
407
  // Run up to 2 attempts: first with session resume, then fresh if stale session detected
399
408
  let _shouldRetry = false;
package/src/cli.js CHANGED
@@ -117,7 +117,7 @@ async function cmdStatus(connector) {
117
117
 
118
118
  async function cmdCreate(connector, flags, positional) {
119
119
  const name = positional[0];
120
- if (!name) { print('Usage: agn create <name> [--type <type>]'); return; }
120
+ if (!name) { print('Usage: agn create <name> [--type <type>] [--install]'); return; }
121
121
  const type = flags.type || 'openclaw';
122
122
  const role = flags.role || 'worker';
123
123
 
@@ -128,8 +128,12 @@ async function cmdCreate(connector, flags, positional) {
128
128
  // Signal daemon to pick up the new agent
129
129
  try { connector.sendDaemonCommand('reload'); } catch {}
130
130
 
131
- // Auto-install if not installed
132
131
  if (!connector.isInstalled(type)) {
132
+ if (!flags.install) {
133
+ print(`Runtime '${type}' is not installed. Run: agn install ${type}`);
134
+ return;
135
+ }
136
+
133
137
  print(`Installing ${type}...`);
134
138
  try {
135
139
  await connector.install(type);
@@ -513,6 +517,7 @@ Commands:
513
517
 
514
518
  Options:
515
519
  --config <dir> Config directory (default: ~/.openagents)
520
+ --install Install runtime during create
516
521
  `);
517
522
  }
518
523
 
package/src/config.js CHANGED
@@ -50,13 +50,14 @@ class Config {
50
50
  fs.writeFileSync(this.configFile, serializeYaml(config), 'utf-8');
51
51
  }
52
52
 
53
- addAgent({ name, type, role, path: agentPath }) {
53
+ addAgent({ name, type, role, path: agentPath, env }) {
54
54
  const config = this.load();
55
55
  if (config.agents.some((a) => a.name === name)) {
56
56
  throw new Error(`Agent '${name}' already exists`);
57
57
  }
58
58
  const entry = { name, type: type || 'openclaw', role: role || 'worker' };
59
59
  if (agentPath) entry.path = agentPath;
60
+ if (env && Object.keys(env).length > 0) entry.env = env;
60
61
  config.agents.push(entry);
61
62
  this.save(config);
62
63
  return entry;
@@ -80,6 +81,27 @@ class Config {
80
81
  return agent;
81
82
  }
82
83
 
84
+ updateAgentEnv(name, env) {
85
+ const config = this.load();
86
+ const agent = config.agents.find((a) => a.name === name);
87
+ if (!agent) throw new Error(`Agent '${name}' not found`);
88
+
89
+ const merged = { ...(agent.env || {}), ...(env || {}) };
90
+ const cleaned = {};
91
+ for (const [key, value] of Object.entries(merged)) {
92
+ if (value !== null && value !== undefined && value !== '') cleaned[key] = value;
93
+ }
94
+
95
+ if (Object.keys(cleaned).length > 0) {
96
+ agent.env = cleaned;
97
+ } else {
98
+ delete agent.env;
99
+ }
100
+
101
+ this.save(config);
102
+ return agent.env || {};
103
+ }
104
+
83
105
  setAgentNetwork(agentName, networkSlug) {
84
106
  const config = this.load();
85
107
  const agent = config.agents.find((a) => a.name === agentName);
@@ -191,6 +213,43 @@ class Config {
191
213
  return { lines: [], size: 0 };
192
214
  }
193
215
  }
216
+
217
+ /**
218
+ * Remove log entries whose leading timestamp falls within [start, end].
219
+ * Lines without a parseable timestamp are preserved to avoid deleting
220
+ * stack traces or continuation lines incorrectly.
221
+ * @param {Object} opts - { start, end }
222
+ * @param {string|number|Date} opts.start
223
+ * @param {string|number|Date} opts.end
224
+ * @returns {{ removed: number, remaining: number }}
225
+ */
226
+ clearLogsInRange(opts = {}) {
227
+ const start = normalizeTimeValue(opts.start);
228
+ const end = normalizeTimeValue(opts.end);
229
+
230
+ if (!start || !end) {
231
+ throw new Error('Start time and end time are required');
232
+ }
233
+ if (start.getTime() > end.getTime()) {
234
+ throw new Error('Start time must be before end time');
235
+ }
236
+ if (!fs.existsSync(this.logFile)) {
237
+ return { removed: 0, remaining: 0 };
238
+ }
239
+
240
+ const content = fs.readFileSync(this.logFile, 'utf-8');
241
+ const hasTrailingNewline = content.endsWith('\n');
242
+ const allLines = content.split('\n');
243
+ if (hasTrailingNewline) allLines.pop();
244
+ const { keptLines, removed } = filterLogsByTimeRange(allLines, start, end);
245
+
246
+ const nextContent = keptLines.join('\n') + (hasTrailingNewline && keptLines.length > 0 ? '\n' : '');
247
+ const tempFile = `${this.logFile}.tmp`;
248
+ fs.writeFileSync(tempFile, nextContent, 'utf-8');
249
+ fs.renameSync(tempFile, this.logFile);
250
+
251
+ return { removed, remaining: keptLines.length };
252
+ }
194
253
  }
195
254
 
196
255
  // -- YAML parser (compatible with Python SDK's daemon.yaml format) --
@@ -253,6 +312,115 @@ function parseYaml(text) {
253
312
  return result;
254
313
  }
255
314
 
315
+ function normalizeTimeValue(value) {
316
+ if (value instanceof Date) {
317
+ return Number.isNaN(value.getTime()) ? null : value;
318
+ }
319
+ if (typeof value === 'number') {
320
+ const date = new Date(value);
321
+ return Number.isNaN(date.getTime()) ? null : date;
322
+ }
323
+ if (typeof value === 'string' && value.trim()) {
324
+ const date = new Date(value);
325
+ return Number.isNaN(date.getTime()) ? null : date;
326
+ }
327
+ return null;
328
+ }
329
+
330
+ function filterLogsByTimeRange(lines, start, end) {
331
+ const headerTimes = resolveLogHeaderTimestamps(lines, end);
332
+ let activeRemove = false;
333
+ let removed = 0;
334
+ const keptLines = [];
335
+
336
+ for (let index = 0; index < lines.length; index += 1) {
337
+ const headerTime = headerTimes[index];
338
+ if (headerTime) {
339
+ const time = headerTime.getTime();
340
+ activeRemove = time >= start.getTime() && time <= end.getTime();
341
+ }
342
+
343
+ if (activeRemove) {
344
+ removed += 1;
345
+ } else {
346
+ keptLines.push(lines[index]);
347
+ }
348
+ }
349
+
350
+ return { keptLines, removed };
351
+ }
352
+
353
+ function resolveLogHeaderTimestamps(lines, referenceTime) {
354
+ const resolved = new Array(lines.length).fill(null);
355
+ let currentDay = startOfLocalDay(referenceTime);
356
+ let lastClockSeconds = null;
357
+
358
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
359
+ const token = parseLogTimestampToken(lines[index]);
360
+ if (!token) continue;
361
+
362
+ if (token.kind === 'iso') {
363
+ resolved[index] = token.date;
364
+ currentDay = startOfLocalDay(token.date);
365
+ lastClockSeconds = (
366
+ token.date.getHours() * 3600 +
367
+ token.date.getMinutes() * 60 +
368
+ token.date.getSeconds()
369
+ );
370
+ continue;
371
+ }
372
+
373
+ if (lastClockSeconds !== null && token.seconds > lastClockSeconds) {
374
+ currentDay = addLocalDays(currentDay, -1);
375
+ }
376
+
377
+ resolved[index] = withLocalClock(currentDay, token.seconds);
378
+ lastClockSeconds = token.seconds;
379
+ }
380
+
381
+ return resolved;
382
+ }
383
+
384
+ function parseLogTimestampToken(line) {
385
+ if (!line) return null;
386
+
387
+ const isoMatch = line.match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2}))/);
388
+ if (isoMatch) {
389
+ const date = new Date(isoMatch[1]);
390
+ if (!Number.isNaN(date.getTime())) {
391
+ return { kind: 'iso', date };
392
+ }
393
+ }
394
+
395
+ const clockMatch = line.match(/^\[(\d{2}):(\d{2}):(\d{2})\]/);
396
+ if (clockMatch) {
397
+ return {
398
+ kind: 'clock',
399
+ seconds:
400
+ Number(clockMatch[1]) * 3600 +
401
+ Number(clockMatch[2]) * 60 +
402
+ Number(clockMatch[3]),
403
+ };
404
+ }
405
+
406
+ return null;
407
+ }
408
+
409
+ function startOfLocalDay(date) {
410
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate());
411
+ }
412
+
413
+ function addLocalDays(date, days) {
414
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate() + days);
415
+ }
416
+
417
+ function withLocalClock(day, seconds) {
418
+ const hours = Math.floor(seconds / 3600);
419
+ const minutes = Math.floor((seconds % 3600) / 60);
420
+ const secs = seconds % 60;
421
+ return new Date(day.getFullYear(), day.getMonth(), day.getDate(), hours, minutes, secs);
422
+ }
423
+
256
424
  function parseYamlValue(val) {
257
425
  if (val === '' || val === 'null' || val === '~') return null;
258
426
  if (val === 'true') return true;
package/src/daemon.js CHANGED
@@ -70,7 +70,7 @@ class Daemon {
70
70
  this._writeStatus();
71
71
  this._cachedAgentNames = new Set(agents.map(a => a.name));
72
72
  this._cachedAgentConfigs = {};
73
- for (const a of agents) this._cachedAgentConfigs[a.name] = a.network || '';
73
+ for (const a of agents) this._cachedAgentConfigs[a.name] = this._agentConfigFingerprint(a);
74
74
  this._log(`Daemon started with ${agents.length} agent(s)`);
75
75
 
76
76
  // Block until shutdown
@@ -553,11 +553,19 @@ class Daemon {
553
553
  _buildAgentEnv(agentCfg) {
554
554
  const type = agentCfg.type || 'openclaw';
555
555
  const saved = this.envManager.load(type);
556
- const resolved = this.envManager.resolve(type, saved, this.registry);
557
- const merged = { ...saved, ...resolved, ...(agentCfg.env || {}) };
556
+ const mergedSaved = { ...saved, ...(agentCfg.env || {}) };
557
+ const resolved = this.envManager.resolve(type, mergedSaved, this.registry);
558
+ const merged = { ...mergedSaved, ...resolved };
558
559
  return { ...process.env, ...merged };
559
560
  }
560
561
 
562
+ _agentConfigFingerprint(agentCfg) {
563
+ return JSON.stringify({
564
+ network: agentCfg.network || '',
565
+ env: agentCfg.env || {},
566
+ });
567
+ }
568
+
561
569
  // ---------------------------------------------------------------------------
562
570
  // Internal — agent kill
563
571
  // ---------------------------------------------------------------------------
@@ -682,7 +690,7 @@ class Daemon {
682
690
  const newAgents = this.config.getAgents();
683
691
  const newNames = new Set(newAgents.map(a => a.name));
684
692
  const newConfigs = {};
685
- for (const a of newAgents) newConfigs[a.name] = a.network || '';
693
+ for (const a of newAgents) newConfigs[a.name] = this._agentConfigFingerprint(a);
686
694
 
687
695
  // Stop removed agents
688
696
  for (const name of oldNames) {
@@ -698,13 +706,13 @@ class Daemon {
698
706
  await this._ensureAdapterCleared(agent.name);
699
707
  this._launchAgent(agent);
700
708
  this._log(`Reload: started new agent '${agent.name}'`);
701
- } else if ((oldConfigs[agent.name] || '') !== (agent.network || '')) {
702
- // Network config changed — restart agent
709
+ } else if ((oldConfigs[agent.name] || '') !== newConfigs[agent.name]) {
710
+ // Network or env config changed — restart agent
703
711
  await this.stopAgent(agent.name);
704
712
  this._stoppedAgents.delete(agent.name);
705
713
  await this._ensureAdapterCleared(agent.name);
706
714
  this._launchAgent(agent);
707
- this._log(`Reload: restarted '${agent.name}' (network changed)`);
715
+ this._log(`Reload: restarted '${agent.name}' (config changed)`);
708
716
  }
709
717
  }
710
718
 
package/src/index.js CHANGED
@@ -74,22 +74,24 @@ class AgentConnector {
74
74
  const agents = this.config.getAgents();
75
75
  const networks = this.config.getNetworks();
76
76
  return agents.map((a) => {
77
- const agentEnv = this.env.load(a.type);
77
+ const type = a.type || 'openclaw';
78
+ const typeEnv = this.env.load(type);
78
79
  const network = networks.find((n) => n.slug === a.network || n.id === a.network);
79
80
  return {
80
81
  name: a.name,
81
- type: a.type || 'openclaw',
82
+ type,
82
83
  role: a.role || 'worker',
83
84
  network: a.network || null,
84
85
  networkName: network ? (network.name || network.slug) : null,
85
86
  path: a.path || null,
86
- env: { ...agentEnv, ...(a.env || {}) },
87
+ env: { ...typeEnv, ...(a.env || {}) },
88
+ instanceEnv: { ...(a.env || {}) },
87
89
  };
88
90
  });
89
91
  }
90
92
 
91
- addAgent({ name, type, role, path }) {
92
- this.config.addAgent({ name, type: type || 'openclaw', role: role || 'worker', path });
93
+ addAgent({ name, type, role, path, env }) {
94
+ this.config.addAgent({ name, type: type || 'openclaw', role: role || 'worker', path, env });
93
95
  return { success: true };
94
96
  }
95
97
 
@@ -104,6 +106,12 @@ class AgentConnector {
104
106
  return this.env.load(agentType);
105
107
  }
106
108
 
109
+ getAgentInstanceEnv(agentName) {
110
+ const agent = this.config.getAgent(agentName);
111
+ if (!agent) throw new Error(`Agent '${agentName}' not found`);
112
+ return { ...(agent.env || {}) };
113
+ }
114
+
107
115
  saveAgentEnv(agentType, env) {
108
116
  this.env.save(agentType, env);
109
117
  // Configure native auth for agents that need it (e.g. OpenClaw auth-profiles.json)
@@ -117,6 +125,24 @@ class AgentConnector {
117
125
  return { success: true };
118
126
  }
119
127
 
128
+ saveAgentInstanceEnv(agentName, env) {
129
+ const agent = this.config.getAgent(agentName);
130
+ if (!agent) throw new Error(`Agent '${agentName}' not found`);
131
+ const saved = this.config.updateAgentEnv(agentName, env);
132
+
133
+ // Preserve native auth side effects for agents that need them while
134
+ // keeping the model choice scoped to this individual agent.
135
+ try {
136
+ if ((agent.type || 'openclaw') === 'openclaw') {
137
+ const OpenClawAdapter = require('./adapters/openclaw');
138
+ const typeEnv = this.env.load(agent.type || 'openclaw');
139
+ OpenClawAdapter.configureNativeAuth({ ...typeEnv, ...saved });
140
+ }
141
+ } catch {}
142
+
143
+ return { success: true };
144
+ }
145
+
120
146
  resolveAgentEnv(agentType, saved) {
121
147
  return this.env.resolve(agentType, saved, this.registry);
122
148
  }
@@ -199,6 +225,14 @@ class AgentConnector {
199
225
  return this.config.getLogs(agentName, lines);
200
226
  }
201
227
 
228
+ tailLogs(opts = {}) {
229
+ return this.config.tailLogs(opts);
230
+ }
231
+
232
+ clearLogsInRange(opts = {}) {
233
+ return this.config.clearLogsInRange(opts);
234
+ }
235
+
202
236
  // -- Workspace API --
203
237
 
204
238
  async createWorkspace(opts) {
@@ -179,6 +179,29 @@ class WorkspaceClient {
179
179
  return events.map((e) => this._eventToMessage(e));
180
180
  }
181
181
 
182
+ /**
183
+ * Fetch the latest workspace.message.posted event id (head cursor).
184
+ * Used by adapters to skip past existing events on join in O(1) instead
185
+ * of paginating from the start. Returns null if the workspace is empty
186
+ * or the request fails.
187
+ */
188
+ async getHeadEventId(workspaceId, token) {
189
+ try {
190
+ const params = new URLSearchParams({
191
+ network: workspaceId,
192
+ type: 'workspace.message.posted',
193
+ sort: 'desc',
194
+ limit: '1',
195
+ });
196
+ const data = await this._get(`/v1/events?${params}`, this._wsHeaders(token));
197
+ const result = data.data || data;
198
+ const events = (result && result.events) || [];
199
+ return events.length > 0 ? (events[0].id || null) : null;
200
+ } catch {
201
+ return null;
202
+ }
203
+ }
204
+
182
205
  /**
183
206
  * Poll for pending messages targeted at an agent via GET /v1/events.
184
207
  * Returns { messages, cursor } where cursor is the last event ID.