@ai-devkit/agent-manager 0.26.0 → 0.26.2

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.
Files changed (37) hide show
  1. package/dist/__tests__/AgentManager.test.js +37 -0
  2. package/dist/__tests__/AgentManager.test.js.map +1 -1
  3. package/dist/__tests__/adapters/CodexAdapter.test.js +249 -0
  4. package/dist/__tests__/adapters/CodexAdapter.test.js.map +1 -1
  5. package/dist/__tests__/utils/AgentRegistry.test.js +68 -46
  6. package/dist/__tests__/utils/AgentRegistry.test.js.map +1 -1
  7. package/dist/adapters/CodexAdapter.d.ts +9 -1
  8. package/dist/adapters/CodexAdapter.d.ts.map +1 -1
  9. package/dist/adapters/CodexAdapter.js +106 -24
  10. package/dist/adapters/CodexAdapter.js.map +1 -1
  11. package/dist/database/connection.d.ts +22 -0
  12. package/dist/database/connection.d.ts.map +1 -0
  13. package/dist/database/connection.js +58 -0
  14. package/dist/database/connection.js.map +1 -0
  15. package/dist/database/index.d.ts +4 -0
  16. package/dist/database/index.d.ts.map +1 -0
  17. package/dist/database/index.js +4 -0
  18. package/dist/database/index.js.map +1 -0
  19. package/dist/database/migrations/001_initial.sql +12 -0
  20. package/dist/database/schema.d.ts +4 -0
  21. package/dist/database/schema.d.ts.map +1 -0
  22. package/dist/database/schema.js +47 -0
  23. package/dist/database/schema.js.map +1 -0
  24. package/dist/utils/AgentRegistry.d.ts +7 -3
  25. package/dist/utils/AgentRegistry.d.ts.map +1 -1
  26. package/dist/utils/AgentRegistry.js +108 -57
  27. package/dist/utils/AgentRegistry.js.map +1 -1
  28. package/package.json +2 -2
  29. package/src/__tests__/AgentManager.test.ts +37 -0
  30. package/src/__tests__/adapters/CodexAdapter.test.ts +155 -0
  31. package/src/__tests__/utils/AgentRegistry.test.ts +48 -40
  32. package/src/adapters/CodexAdapter.ts +147 -27
  33. package/src/database/connection.ts +74 -0
  34. package/src/database/index.ts +7 -0
  35. package/src/database/migrations/001_initial.sql +12 -0
  36. package/src/database/schema.ts +62 -0
  37. package/src/utils/AgentRegistry.ts +109 -49
@@ -1,7 +1,10 @@
1
- import fs from 'fs';
2
1
  import os from 'os';
3
2
  import path from 'path';
4
3
  import type { AgentType } from '../adapters/AgentAdapter.js';
4
+ import {
5
+ DatabaseConnection,
6
+ resolveAgentRegistryDbPath,
7
+ } from '../database/index.js';
5
8
 
6
9
  export class RenameNotFoundError extends Error {
7
10
  constructor(public agentName: string) {
@@ -28,8 +31,16 @@ export interface RegistryEntry {
28
31
  sessionFilePath: string;
29
32
  }
30
33
 
31
- interface RegistryFile {
32
- entries: RegistryEntry[];
34
+ interface RegistryRow {
35
+ name: string;
36
+ type: AgentType;
37
+ pid: number;
38
+ tmux_session: string;
39
+ cwd: string;
40
+ started_at: string;
41
+ session_id: string;
42
+ session_file_path: string;
43
+ updated_at: string;
33
44
  }
34
45
 
35
46
  const DEFAULT_REGISTRY_PATH = path.join(os.homedir(), '.ai-devkit', 'agents.json');
@@ -37,10 +48,10 @@ const DEFAULT_REGISTRY_PATH = path.join(os.homedir(), '.ai-devkit', 'agents.json
37
48
  let defaultInstance: AgentRegistry | null = null;
38
49
 
39
50
  export class AgentRegistry {
40
- private filePath: string;
51
+ private db: DatabaseConnection;
41
52
 
42
53
  constructor(filePath: string = DEFAULT_REGISTRY_PATH) {
43
- this.filePath = filePath;
54
+ this.db = new DatabaseConnection({ dbPath: resolveAgentRegistryDbPath(filePath) });
44
55
  }
45
56
 
46
57
  static default(): AgentRegistry {
@@ -50,32 +61,79 @@ export class AgentRegistry {
50
61
  return defaultInstance;
51
62
  }
52
63
 
53
- private readFile(): RegistryFile {
54
- try {
55
- const raw = fs.readFileSync(this.filePath, 'utf8');
56
- const parsed = JSON.parse(raw) as RegistryFile;
57
- return { entries: Array.isArray(parsed.entries) ? parsed.entries : [] };
58
- } catch {
59
- return { entries: [] };
60
- }
61
- }
62
-
63
- private writeFile(data: RegistryFile): void {
64
- const dir = path.dirname(this.filePath);
65
- fs.mkdirSync(dir, { recursive: true });
66
- const tmp = `${this.filePath}.tmp`;
67
- fs.writeFileSync(tmp, JSON.stringify(data, null, 2), 'utf8');
68
- fs.renameSync(tmp, this.filePath);
64
+ private rowToEntry(row: RegistryRow): RegistryEntry {
65
+ return {
66
+ name: row.name,
67
+ type: row.type,
68
+ pid: row.pid,
69
+ tmuxSession: row.tmux_session,
70
+ cwd: row.cwd,
71
+ startedAt: row.started_at,
72
+ sessionId: row.session_id,
73
+ sessionFilePath: row.session_file_path,
74
+ };
69
75
  }
70
76
 
71
77
  private mergeEntry(incoming: RegistryEntry, existing: RegistryEntry | undefined): RegistryEntry {
72
78
  if (!existing) return incoming;
79
+ const incomingIsManaged = Boolean(incoming.tmuxSession);
73
80
  return {
74
- ...incoming,
81
+ ...existing,
82
+ name: incomingIsManaged ? incoming.name : existing.name,
75
83
  tmuxSession: incoming.tmuxSession || existing.tmuxSession,
84
+ cwd: incoming.cwd || existing.cwd,
85
+ startedAt: existing.startedAt || incoming.startedAt,
86
+ sessionId: incoming.sessionId || existing.sessionId,
87
+ sessionFilePath: incoming.sessionFilePath || existing.sessionFilePath,
76
88
  };
77
89
  }
78
90
 
91
+ private findByIdentity(type: AgentType, pid: number): RegistryEntry | undefined {
92
+ const row = this.db.queryOne<RegistryRow>(
93
+ 'SELECT * FROM agents WHERE type = ? AND pid = ?',
94
+ [type, pid],
95
+ );
96
+ return row ? this.rowToEntry(row) : undefined;
97
+ }
98
+
99
+ private findByName(name: string): RegistryEntry | undefined {
100
+ const row = this.db.queryOne<RegistryRow>('SELECT * FROM agents WHERE name = ?', [name]);
101
+ return row ? this.rowToEntry(row) : undefined;
102
+ }
103
+
104
+ private deleteNameConflict(name: string, type: AgentType, pid: number): void {
105
+ const conflict = this.findByName(name);
106
+ if (!conflict) return;
107
+ if (conflict.type === type && conflict.pid === pid) return;
108
+ if (!this.isAlive(conflict)) {
109
+ this.db.execute('DELETE FROM agents WHERE type = ? AND pid = ?', [conflict.type, conflict.pid]);
110
+ }
111
+ }
112
+
113
+ private insertOrUpdate(entry: RegistryEntry): void {
114
+ this.db.instance.prepare(`
115
+ INSERT INTO agents (
116
+ type, pid, name, tmux_session, cwd, started_at, session_id, session_file_path, updated_at
117
+ )
118
+ VALUES (
119
+ @type, @pid, @name, @tmuxSession, @cwd, @startedAt, @sessionId, @sessionFilePath, @updatedAt
120
+ )
121
+ ON CONFLICT(type, pid) DO UPDATE SET
122
+ name = excluded.name,
123
+ tmux_session = excluded.tmux_session,
124
+ cwd = excluded.cwd,
125
+ started_at = agents.started_at,
126
+ session_id = excluded.session_id,
127
+ session_file_path = excluded.session_file_path,
128
+ updated_at = excluded.updated_at
129
+ `).run({ ...entry, updatedAt: new Date().toISOString() });
130
+ }
131
+
132
+ private save(entry: RegistryEntry): void {
133
+ this.deleteNameConflict(entry.name, entry.type, entry.pid);
134
+ this.insertOrUpdate(entry);
135
+ }
136
+
79
137
  isAlive(entry: RegistryEntry): boolean {
80
138
  try {
81
139
  process.kill(entry.pid, 0);
@@ -86,11 +144,13 @@ export class AgentRegistry {
86
144
  }
87
145
 
88
146
  prune(): void {
89
- const data = this.readFile();
90
- const live = data.entries.filter((e) => this.isAlive(e));
91
- if (live.length !== data.entries.length) {
92
- this.writeFile({ entries: live });
93
- }
147
+ const entries = this.list();
148
+ const stale = entries.filter((e) => !this.isAlive(e));
149
+ this.db.transaction(() => {
150
+ for (const entry of stale) {
151
+ this.db.execute('DELETE FROM agents WHERE type = ? AND pid = ?', [entry.type, entry.pid]);
152
+ }
153
+ });
94
154
  }
95
155
 
96
156
  register(entry: RegistryEntry): void {
@@ -99,41 +159,41 @@ export class AgentRegistry {
99
159
 
100
160
  registerBatch(entries: RegistryEntry[]): void {
101
161
  if (entries.length === 0) return;
102
- const data = this.readFile();
103
- for (const incoming of entries) {
104
- const idx = data.entries.findIndex((e) => e.name === incoming.name);
105
- if (idx >= 0) {
106
- data.entries[idx] = this.mergeEntry(incoming, data.entries[idx]);
107
- } else {
108
- data.entries.push(incoming);
162
+ this.db.transaction(() => {
163
+ for (const incoming of entries) {
164
+ const existing = this.findByIdentity(incoming.type, incoming.pid);
165
+ this.save(this.mergeEntry(incoming, existing));
109
166
  }
110
- }
111
- this.writeFile(data);
167
+ });
112
168
  }
113
169
 
114
170
  rename(currentName: string, newName: string): void {
115
- const data = this.readFile();
116
- const idx = data.entries.findIndex((e) => e.name === currentName);
117
- if (idx < 0) {
171
+ const existing = this.findByName(currentName);
172
+ if (!existing) {
118
173
  throw new RenameNotFoundError(currentName);
119
174
  }
120
- const liveEntries = data.entries.filter((e) => this.isAlive(e));
121
- const conflict = liveEntries.find((e) => e.name === newName);
122
- if (conflict) {
175
+ const conflict = this.findByName(newName);
176
+ if (conflict && this.isAlive(conflict)) {
123
177
  throw new RenameConflictError(newName);
124
178
  }
125
- const pruned = liveEntries.map((e) =>
126
- e.name === currentName ? { ...e, name: newName } : e,
127
- );
128
- this.writeFile({ entries: pruned });
179
+
180
+ this.db.transaction(() => {
181
+ if (conflict) {
182
+ this.db.execute('DELETE FROM agents WHERE type = ? AND pid = ?', [conflict.type, conflict.pid]);
183
+ }
184
+ this.db.execute(
185
+ 'UPDATE agents SET name = ?, updated_at = ? WHERE type = ? AND pid = ?',
186
+ [newName, new Date().toISOString(), existing.type, existing.pid],
187
+ );
188
+ });
129
189
  }
130
190
 
131
191
  lookup(name: string): RegistryEntry | null {
132
- const data = this.readFile();
133
- return data.entries.find((e) => e.name === name) ?? null;
192
+ return this.findByName(name) ?? null;
134
193
  }
135
194
 
136
195
  list(): RegistryEntry[] {
137
- return this.readFile().entries;
196
+ const rows = this.db.query<RegistryRow>('SELECT * FROM agents ORDER BY started_at ASC, name ASC');
197
+ return rows.map((row) => this.rowToEntry(row));
138
198
  }
139
199
  }