@automagik/genie 4.260331.5 → 4.260331.7
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/.claude-plugin/marketplace.json +1 -1
- package/CLAUDE.md +31 -5
- package/dist/genie.js +600 -691
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/plugins/genie/.claude-plugin/plugin.json +1 -1
- package/plugins/genie/package.json +1 -1
- package/skills/genie/SKILL.md +71 -233
- package/skills/genie/reference/lifecycle.md +65 -0
- package/src/db/migrations/015_agent_archived_state.sql +0 -20
- package/src/db/migrations/018_drop_app_store.sql +4 -0
- package/src/genie.ts +26 -14
- package/src/lib/agent-sync.ts +55 -194
- package/src/lib/export-format.ts +1 -12
- package/src/lib/import-order.ts +1 -11
- package/src/term-commands/agent/directory.ts +2 -37
- package/src/term-commands/agent/index.ts +6 -0
- package/src/term-commands/dir.ts +3 -160
- package/src/term-commands/export.ts +0 -13
- package/src/term-commands/msg.ts +8 -0
- package/src/term-commands/team.ts +8 -0
- package/src/lib/agent-cache.ts +0 -282
- package/src/lib/manifest.ts +0 -342
- package/src/term-commands/install.ts +0 -372
- package/src/term-commands/item-uninstall.ts +0 -118
- package/src/term-commands/item-update.ts +0 -205
- package/src/term-commands/publish.ts +0 -187
package/src/lib/agent-sync.ts
CHANGED
|
@@ -3,13 +3,12 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Scans {workspace}/agents/ for directories containing AGENTS.md (the discovery marker).
|
|
5
5
|
* For each found agent, reads the git remote to derive repo_path, then registers or
|
|
6
|
-
* updates the entry in the
|
|
6
|
+
* updates the entry in the agent directory.
|
|
7
7
|
*
|
|
8
8
|
* Handles lifecycle:
|
|
9
|
-
* - New agent dir → register in
|
|
9
|
+
* - New agent dir → register in agent directory
|
|
10
10
|
* - Existing agent with stale data → update
|
|
11
|
-
* -
|
|
12
|
-
* - Missing agent dir → archive (never delete — preserves history)
|
|
11
|
+
* - Missing agent dir → remove
|
|
13
12
|
*
|
|
14
13
|
* Used by:
|
|
15
14
|
* - `genie serve` (startup sync + file watcher)
|
|
@@ -20,13 +19,7 @@
|
|
|
20
19
|
import { execSync } from 'node:child_process';
|
|
21
20
|
import { existsSync, watch as fsWatch, readdirSync, realpathSync } from 'node:fs';
|
|
22
21
|
import { join } from 'node:path';
|
|
23
|
-
import
|
|
24
|
-
getItemFromStore,
|
|
25
|
-
listItemsFromStore,
|
|
26
|
-
regenerateAgentCache,
|
|
27
|
-
registerItemInStore,
|
|
28
|
-
updateItemInStore,
|
|
29
|
-
} from './agent-cache.js';
|
|
22
|
+
import * as directory from './agent-directory.js';
|
|
30
23
|
|
|
31
24
|
// ============================================================================
|
|
32
25
|
// Types
|
|
@@ -128,98 +121,6 @@ function discoverSingleAgent(workspaceRoot: string, agentName: string): AgentInf
|
|
|
128
121
|
};
|
|
129
122
|
}
|
|
130
123
|
|
|
131
|
-
// ============================================================================
|
|
132
|
-
// Archive / Reactivation helpers
|
|
133
|
-
// ============================================================================
|
|
134
|
-
|
|
135
|
-
/** Archive an agent in both agents table and app_store. Never deletes rows. */
|
|
136
|
-
async function archiveAgent(name: string): Promise<boolean> {
|
|
137
|
-
let archived = false;
|
|
138
|
-
try {
|
|
139
|
-
const { getConnection } = await import('./db.js');
|
|
140
|
-
const sql = await getConnection();
|
|
141
|
-
// Archive in agents table (set state='archived')
|
|
142
|
-
const result = await sql`
|
|
143
|
-
UPDATE agents SET state = 'archived', updated_at = now()
|
|
144
|
-
WHERE (custom_name = ${name} OR role = ${name})
|
|
145
|
-
AND (state IS NULL OR state != 'archived')
|
|
146
|
-
`;
|
|
147
|
-
if (result.count > 0) archived = true;
|
|
148
|
-
} catch {
|
|
149
|
-
// DB may not be available
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Archive in app_store (set approval_status='archived')
|
|
153
|
-
try {
|
|
154
|
-
const existing = await getItemFromStore(name).catch(() => null);
|
|
155
|
-
if (existing && existing.approval_status !== 'archived') {
|
|
156
|
-
await updateItemInStore(name, {
|
|
157
|
-
manifest: {
|
|
158
|
-
...(existing.manifest as Record<string, unknown>),
|
|
159
|
-
archived: true,
|
|
160
|
-
archivedAt: new Date().toISOString(),
|
|
161
|
-
},
|
|
162
|
-
});
|
|
163
|
-
archived = true;
|
|
164
|
-
}
|
|
165
|
-
} catch {
|
|
166
|
-
// Best-effort
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
return archived;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/** Reactivate an archived agent with full backfill. */
|
|
173
|
-
async function reactivateAgent(agent: AgentInfo): Promise<void> {
|
|
174
|
-
const orgRepo = agent.repoUrl ? extractOrgRepo(agent.repoUrl) : null;
|
|
175
|
-
const repoPath = orgRepo ?? agent.repoUrl ?? agent.dir;
|
|
176
|
-
|
|
177
|
-
// Update app_store entry — clear archived flag, refresh paths
|
|
178
|
-
const existing = await getItemFromStore(agent.name).catch(() => null);
|
|
179
|
-
if (existing) {
|
|
180
|
-
const manifest = { ...(existing.manifest as Record<string, unknown>) };
|
|
181
|
-
manifest.archived = undefined;
|
|
182
|
-
manifest.archivedAt = undefined;
|
|
183
|
-
manifest.repo = repoPath;
|
|
184
|
-
manifest.productRepo = agent.productRepo;
|
|
185
|
-
manifest.source = 'auto-sync';
|
|
186
|
-
await updateItemInStore(agent.name, {
|
|
187
|
-
installPath: agent.dir,
|
|
188
|
-
gitUrl: agent.repoUrl ?? undefined,
|
|
189
|
-
manifest,
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Reactivate in agents table
|
|
194
|
-
try {
|
|
195
|
-
const { getConnection } = await import('./db.js');
|
|
196
|
-
const sql = await getConnection();
|
|
197
|
-
await sql`
|
|
198
|
-
UPDATE agents SET state = 'idle', repo_path = ${repoPath}, updated_at = now()
|
|
199
|
-
WHERE (custom_name = ${agent.name} OR role = ${agent.name})
|
|
200
|
-
AND state = 'archived'
|
|
201
|
-
`;
|
|
202
|
-
} catch {
|
|
203
|
-
// DB may not be available
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Trigger session backfill for conversation history recovery
|
|
207
|
-
await triggerSessionBackfill(agent).catch(() => {});
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/** Trigger session backfill for a reactivated agent. */
|
|
211
|
-
async function triggerSessionBackfill(_agent: AgentInfo): Promise<void> {
|
|
212
|
-
try {
|
|
213
|
-
const { getConnection, isAvailable } = await import('./db.js');
|
|
214
|
-
if (!(await isAvailable())) return;
|
|
215
|
-
const sql = await getConnection();
|
|
216
|
-
const { startBackfill } = await import('./session-backfill.js');
|
|
217
|
-
await startBackfill(sql);
|
|
218
|
-
} catch {
|
|
219
|
-
// Best-effort — backfill may not be ready
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
124
|
// ============================================================================
|
|
224
125
|
// Sync
|
|
225
126
|
// ============================================================================
|
|
@@ -227,9 +128,8 @@ async function triggerSessionBackfill(_agent: AgentInfo): Promise<void> {
|
|
|
227
128
|
/**
|
|
228
129
|
* Sync all agents from {workspaceRoot}/agents/ into the directory.
|
|
229
130
|
* - New agents → registered
|
|
230
|
-
* - Existing agents with stale
|
|
231
|
-
* -
|
|
232
|
-
* - Agents in store whose dirs are gone → archived
|
|
131
|
+
* - Existing agents with stale data → updated
|
|
132
|
+
* - Agents whose dirs are gone → removed
|
|
233
133
|
*
|
|
234
134
|
* Idempotent — safe to run repeatedly.
|
|
235
135
|
*/
|
|
@@ -249,15 +149,8 @@ export async function syncAgentDirectory(workspaceRoot: string): Promise<SyncRes
|
|
|
249
149
|
}
|
|
250
150
|
}
|
|
251
151
|
|
|
252
|
-
//
|
|
253
|
-
await
|
|
254
|
-
|
|
255
|
-
// Regenerate cache after mutations
|
|
256
|
-
const hasMutations =
|
|
257
|
-
result.registered.length + result.updated.length + result.archived.length + result.reactivated.length;
|
|
258
|
-
if (hasMutations > 0) {
|
|
259
|
-
await regenerateAgentCache().catch(() => {});
|
|
260
|
-
}
|
|
152
|
+
// Remove agents whose dirs no longer exist
|
|
153
|
+
await removeMissingAgents(discoveredNames, result);
|
|
261
154
|
|
|
262
155
|
return result;
|
|
263
156
|
}
|
|
@@ -267,58 +160,30 @@ export function printSyncResult(result: SyncResult): void {
|
|
|
267
160
|
if (result.registered.length > 0) console.log(` Registered: ${result.registered.join(', ')}`);
|
|
268
161
|
if (result.updated.length > 0) console.log(` Updated: ${result.updated.join(', ')}`);
|
|
269
162
|
if (result.reactivated.length > 0) console.log(` Reactivated: ${result.reactivated.join(', ')}`);
|
|
270
|
-
if (result.archived.length > 0) console.log(`
|
|
163
|
+
if (result.archived.length > 0) console.log(` Removed: ${result.archived.join(', ')}`);
|
|
271
164
|
if (result.unchanged.length > 0) console.log(` Unchanged: ${result.unchanged.join(', ')}`);
|
|
272
165
|
for (const err of result.errors) {
|
|
273
166
|
console.error(` Error (${err.name}): ${err.error}`);
|
|
274
167
|
}
|
|
275
168
|
const total = result.registered.length + result.updated.length + result.unchanged.length + result.reactivated.length;
|
|
276
|
-
console.log(`\nSync complete: ${total} active agent(s), ${result.archived.length}
|
|
169
|
+
console.log(`\nSync complete: ${total} active agent(s), ${result.archived.length} removed.`);
|
|
277
170
|
}
|
|
278
171
|
|
|
279
|
-
/**
|
|
280
|
-
async function
|
|
281
|
-
workspaceRoot: string,
|
|
282
|
-
discoveredNames: Set<string>,
|
|
283
|
-
result: SyncResult,
|
|
284
|
-
): Promise<void> {
|
|
172
|
+
/** Remove directory entries whose agent dirs no longer exist on disk. */
|
|
173
|
+
async function removeMissingAgents(discoveredNames: Set<string>, result: SyncResult): Promise<void> {
|
|
285
174
|
try {
|
|
286
|
-
const
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
if (
|
|
291
|
-
const manifest = (item.manifest ?? {}) as Record<string, unknown>;
|
|
292
|
-
if (manifest.archived) continue; // already archived
|
|
293
|
-
if (manifest.source !== 'auto-sync') continue; // only archive auto-synced agents
|
|
294
|
-
|
|
295
|
-
// Verify the agent was from this workspace
|
|
296
|
-
if (item.install_path && !item.install_path.startsWith(agentsDir)) continue;
|
|
175
|
+
const entries = await directory.ls();
|
|
176
|
+
for (const entry of entries) {
|
|
177
|
+
if (discoveredNames.has(entry.name)) continue;
|
|
178
|
+
if (entry.scope === 'built-in') continue;
|
|
179
|
+
if (!entry.dir || !entry.dir.includes('/agents/')) continue; // only remove auto-synced
|
|
297
180
|
|
|
298
|
-
const
|
|
299
|
-
if (
|
|
181
|
+
const removed = await directory.rm(entry.name);
|
|
182
|
+
if (removed) result.archived.push(entry.name);
|
|
300
183
|
}
|
|
301
184
|
} catch {
|
|
302
|
-
// Best-effort
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
/** Sync a single agent by name from the workspace (used by file watcher). */
|
|
307
|
-
async function syncSingleAgentByName(workspaceRoot: string, agentName: string): Promise<string> {
|
|
308
|
-
const agent = discoverSingleAgent(workspaceRoot, agentName);
|
|
309
|
-
if (!agent) return 'not-found';
|
|
310
|
-
|
|
311
|
-
const result: SyncResult = { registered: [], updated: [], unchanged: [], archived: [], reactivated: [], errors: [] };
|
|
312
|
-
await syncSingleAgent(agent, result);
|
|
313
|
-
|
|
314
|
-
if (result.registered.length > 0 || result.updated.length > 0 || result.reactivated.length > 0) {
|
|
315
|
-
await regenerateAgentCache().catch(() => {});
|
|
185
|
+
// Best-effort
|
|
316
186
|
}
|
|
317
|
-
|
|
318
|
-
if (result.reactivated.length > 0) return 'reactivated';
|
|
319
|
-
if (result.registered.length > 0) return 'registered';
|
|
320
|
-
if (result.updated.length > 0) return 'updated';
|
|
321
|
-
return 'unchanged';
|
|
322
187
|
}
|
|
323
188
|
|
|
324
189
|
/** Core sync logic for a single agent. */
|
|
@@ -326,39 +191,26 @@ async function syncSingleAgent(agent: AgentInfo, result: SyncResult): Promise<vo
|
|
|
326
191
|
const orgRepo = agent.repoUrl ? extractOrgRepo(agent.repoUrl) : null;
|
|
327
192
|
const repoPath = orgRepo ?? agent.repoUrl ?? agent.dir;
|
|
328
193
|
|
|
329
|
-
const existing = await
|
|
194
|
+
const existing = await directory.get(agent.name);
|
|
330
195
|
|
|
331
196
|
if (!existing) {
|
|
332
|
-
await
|
|
197
|
+
await directory.add({
|
|
333
198
|
name: agent.name,
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
manifest: { promptMode: 'append', repo: repoPath, productRepo: agent.productRepo, source: 'auto-sync' },
|
|
199
|
+
dir: agent.dir,
|
|
200
|
+
repo: repoPath,
|
|
201
|
+
promptMode: 'append',
|
|
338
202
|
});
|
|
339
203
|
result.registered.push(agent.name);
|
|
340
204
|
return;
|
|
341
205
|
}
|
|
342
206
|
|
|
343
|
-
// Check if this is a reactivation (was archived)
|
|
344
|
-
const manifest = (existing.manifest ?? {}) as Record<string, unknown>;
|
|
345
|
-
if (manifest.archived) {
|
|
346
|
-
await reactivateAgent(agent);
|
|
347
|
-
result.reactivated.push(agent.name);
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
207
|
// Check if update needed
|
|
352
|
-
const needsUpdate =
|
|
353
|
-
(manifest.repo as string) !== repoPath ||
|
|
354
|
-
existing.install_path !== agent.dir ||
|
|
355
|
-
(agent.productRepo && manifest.productRepo !== agent.productRepo);
|
|
208
|
+
const needsUpdate = existing.repo !== repoPath || existing.dir !== agent.dir;
|
|
356
209
|
|
|
357
210
|
if (needsUpdate) {
|
|
358
|
-
await
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
manifest: { ...manifest, repo: repoPath, productRepo: agent.productRepo, source: 'auto-sync' },
|
|
211
|
+
await directory.edit(agent.name, {
|
|
212
|
+
dir: agent.dir,
|
|
213
|
+
repo: repoPath,
|
|
362
214
|
});
|
|
363
215
|
result.updated.push(agent.name);
|
|
364
216
|
} else {
|
|
@@ -374,21 +226,17 @@ interface AgentWatcher {
|
|
|
374
226
|
close: () => void;
|
|
375
227
|
}
|
|
376
228
|
|
|
377
|
-
/**
|
|
378
|
-
async function
|
|
379
|
-
const
|
|
380
|
-
if (
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
return 'archived';
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
return null;
|
|
229
|
+
/** Sync a single agent by name from the workspace (used by file watcher). */
|
|
230
|
+
async function syncSingleAgentByName(workspaceRoot: string, agentName: string): Promise<string> {
|
|
231
|
+
const agent = discoverSingleAgent(workspaceRoot, agentName);
|
|
232
|
+
if (!agent) return 'not-found';
|
|
233
|
+
|
|
234
|
+
const result: SyncResult = { registered: [], updated: [], unchanged: [], archived: [], reactivated: [], errors: [] };
|
|
235
|
+
await syncSingleAgent(agent, result);
|
|
236
|
+
|
|
237
|
+
if (result.registered.length > 0) return 'registered';
|
|
238
|
+
if (result.updated.length > 0) return 'updated';
|
|
239
|
+
return 'unchanged';
|
|
392
240
|
}
|
|
393
241
|
|
|
394
242
|
/**
|
|
@@ -396,8 +244,7 @@ async function processWatchedAgent(workspaceRoot: string, agentsDir: string, nam
|
|
|
396
244
|
* Debounces changes with a 2s window.
|
|
397
245
|
*
|
|
398
246
|
* - New directory with AGENTS.md → auto-register
|
|
399
|
-
* -
|
|
400
|
-
* - Removed directory → archive (never delete)
|
|
247
|
+
* - Removed directory → remove from directory
|
|
401
248
|
*/
|
|
402
249
|
export function watchAgentDirectory(
|
|
403
250
|
workspaceRoot: string,
|
|
@@ -440,3 +287,17 @@ export function watchAgentDirectory(
|
|
|
440
287
|
},
|
|
441
288
|
};
|
|
442
289
|
}
|
|
290
|
+
|
|
291
|
+
/** Process a single watched agent change. */
|
|
292
|
+
async function processWatchedAgent(workspaceRoot: string, agentsDir: string, name: string): Promise<string | null> {
|
|
293
|
+
const agentDir = join(agentsDir, name);
|
|
294
|
+
if (existsSync(agentDir) && existsSync(join(agentDir, 'AGENTS.md'))) {
|
|
295
|
+
const action = await syncSingleAgentByName(workspaceRoot, name);
|
|
296
|
+
return action !== 'unchanged' && action !== 'not-found' ? action : null;
|
|
297
|
+
}
|
|
298
|
+
if (!existsSync(agentDir)) {
|
|
299
|
+
const removed = await directory.rm(name);
|
|
300
|
+
if (removed) return 'removed';
|
|
301
|
+
}
|
|
302
|
+
return null;
|
|
303
|
+
}
|
package/src/lib/export-format.ts
CHANGED
|
@@ -18,16 +18,7 @@ export interface ExportDocument {
|
|
|
18
18
|
data: Record<string, unknown[]>;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
export type ExportGroup =
|
|
22
|
-
| 'boards'
|
|
23
|
-
| 'tasks'
|
|
24
|
-
| 'tags'
|
|
25
|
-
| 'projects'
|
|
26
|
-
| 'schedules'
|
|
27
|
-
| 'agents'
|
|
28
|
-
| 'apps'
|
|
29
|
-
| 'comms'
|
|
30
|
-
| 'config';
|
|
21
|
+
export type ExportGroup = 'boards' | 'tasks' | 'tags' | 'projects' | 'schedules' | 'agents' | 'comms' | 'config';
|
|
31
22
|
|
|
32
23
|
/** Tables belonging to each export group */
|
|
33
24
|
export const GROUP_TABLES: Record<ExportGroup, string[]> = {
|
|
@@ -37,7 +28,6 @@ export const GROUP_TABLES: Record<ExportGroup, string[]> = {
|
|
|
37
28
|
projects: ['projects'],
|
|
38
29
|
schedules: ['schedules'],
|
|
39
30
|
agents: ['agents', 'agent_templates', 'agent_checkpoints'],
|
|
40
|
-
apps: ['app_store', 'installed_apps', 'app_versions'],
|
|
41
31
|
comms: ['conversations', 'conversation_members', 'messages', 'mailbox', 'team_chat', 'notification_preferences'],
|
|
42
32
|
config: ['os_config', 'instances', 'warm_pool', 'golden_images'],
|
|
43
33
|
};
|
|
@@ -49,7 +39,6 @@ export const ALL_GROUPS: ExportGroup[] = [
|
|
|
49
39
|
'projects',
|
|
50
40
|
'schedules',
|
|
51
41
|
'agents',
|
|
52
|
-
'apps',
|
|
53
42
|
'comms',
|
|
54
43
|
'config',
|
|
55
44
|
];
|
package/src/lib/import-order.ts
CHANGED
|
@@ -21,23 +21,13 @@ const IMPORT_LEVELS: string[][] = [
|
|
|
21
21
|
'task_types',
|
|
22
22
|
'notification_preferences',
|
|
23
23
|
// Optional (KhalOS)
|
|
24
|
-
'app_store',
|
|
25
24
|
'os_config',
|
|
26
25
|
'golden_images',
|
|
27
26
|
'warm_pool',
|
|
28
27
|
'instances',
|
|
29
28
|
],
|
|
30
29
|
// Level 1: Depend on Level 0
|
|
31
|
-
[
|
|
32
|
-
'triggers',
|
|
33
|
-
'boards',
|
|
34
|
-
'board_templates',
|
|
35
|
-
'agents',
|
|
36
|
-
'conversations',
|
|
37
|
-
// Optional (KhalOS)
|
|
38
|
-
'installed_apps',
|
|
39
|
-
'app_versions',
|
|
40
|
-
],
|
|
30
|
+
['triggers', 'boards', 'board_templates', 'agents', 'conversations'],
|
|
41
31
|
// Level 2: Depend on Level 1
|
|
42
32
|
['tasks', 'runs', 'messages', 'conversation_members', 'mailbox', 'team_chat'],
|
|
43
33
|
// Level 3: Depend on Level 2
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { Command } from 'commander';
|
|
7
|
-
import { type StoreRow, listItemsFromStore, migrateAgentDirectory } from '../../lib/agent-cache.js';
|
|
8
7
|
import * as directory from '../../lib/agent-directory.js';
|
|
9
8
|
import { printSyncResult, syncAgentDirectory } from '../../lib/agent-sync.js';
|
|
10
9
|
import { ALL_BUILTINS } from '../../lib/builtin-agents.js';
|
|
@@ -82,42 +81,8 @@ function printBuiltinAgentsTable(): void {
|
|
|
82
81
|
console.log('');
|
|
83
82
|
}
|
|
84
83
|
|
|
85
|
-
function
|
|
86
|
-
|
|
87
|
-
return roles
|
|
88
|
-
.flatMap((r) => r.split(','))
|
|
89
|
-
.map((r) => r.trim())
|
|
90
|
-
.filter(Boolean);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
async function listEntries(json?: boolean, includeBuiltins?: boolean, includeArchived?: boolean): Promise<void> {
|
|
94
|
-
await migrateAgentDirectory().catch(() => {});
|
|
95
|
-
|
|
96
|
-
let entries: directory.ScopedDirectoryEntry[];
|
|
97
|
-
try {
|
|
98
|
-
const storeItems = await listItemsFromStore('agent');
|
|
99
|
-
entries = storeItems
|
|
100
|
-
.filter((item: StoreRow) => {
|
|
101
|
-
if (includeArchived) return true;
|
|
102
|
-
const manifest = (item.manifest ?? {}) as Record<string, unknown>;
|
|
103
|
-
return !manifest.archived;
|
|
104
|
-
})
|
|
105
|
-
.map((item: StoreRow) => {
|
|
106
|
-
const manifest = (item.manifest ?? {}) as Record<string, unknown>;
|
|
107
|
-
return {
|
|
108
|
-
name: item.name,
|
|
109
|
-
dir: (item.install_path as string) ?? '',
|
|
110
|
-
repo: (manifest.repo as string) ?? '',
|
|
111
|
-
promptMode: ((manifest.promptMode as string) ?? 'append') as directory.PromptMode,
|
|
112
|
-
model: manifest.model as string | undefined,
|
|
113
|
-
roles: normalizeRoles(manifest.roles as string[] | undefined),
|
|
114
|
-
registeredAt: item.installed_at as string,
|
|
115
|
-
scope: (manifest.archived ? 'archived' : 'global') as directory.DirectoryScope,
|
|
116
|
-
};
|
|
117
|
-
});
|
|
118
|
-
} catch {
|
|
119
|
-
entries = await directory.ls();
|
|
120
|
-
}
|
|
84
|
+
async function listEntries(json?: boolean, includeBuiltins?: boolean, _includeArchived?: boolean): Promise<void> {
|
|
85
|
+
const entries = await directory.ls();
|
|
121
86
|
|
|
122
87
|
if (json) {
|
|
123
88
|
const result: Record<string, unknown>[] = entries.map((e) => ({ ...e, builtin: false }));
|
|
@@ -37,4 +37,10 @@ export function registerAgentCommands(program: Command): void {
|
|
|
37
37
|
registerAgentBrief(agent);
|
|
38
38
|
registerAgentLog(agent);
|
|
39
39
|
registerAgentSend(agent);
|
|
40
|
+
|
|
41
|
+
agent.on('command:*', (operands: string[]) => {
|
|
42
|
+
const cmd = operands[0];
|
|
43
|
+
const available = agent.commands.map((c) => c.name()).join(', ');
|
|
44
|
+
agent.error(`Unknown agent command '${cmd}'. Available: ${available}`);
|
|
45
|
+
});
|
|
40
46
|
}
|
package/src/term-commands/dir.ts
CHANGED
|
@@ -12,28 +12,16 @@
|
|
|
12
12
|
* Commands:
|
|
13
13
|
* genie agent register <name> — Register agent locally + auto-register in Omni
|
|
14
14
|
*
|
|
15
|
-
* Storage:
|
|
16
|
-
* Legacy `agents` table kept for backward compat with spawn.
|
|
17
|
-
* JSON cache (~/.genie/agent-directory.json) regenerated after every mutation.
|
|
15
|
+
* Storage: agent-directory.json is the source of truth for registered agents.
|
|
18
16
|
*/
|
|
19
17
|
|
|
20
18
|
import { resolve as resolvePath } from 'node:path';
|
|
21
19
|
import type { Command } from 'commander';
|
|
22
|
-
import {
|
|
23
|
-
type StoreRow,
|
|
24
|
-
listItemsFromStore,
|
|
25
|
-
migrateAgentDirectory,
|
|
26
|
-
regenerateAgentCache,
|
|
27
|
-
registerItemInStore,
|
|
28
|
-
removeItemFromStore,
|
|
29
|
-
updateItemInStore,
|
|
30
|
-
} from '../lib/agent-cache.js';
|
|
31
20
|
import * as directory from '../lib/agent-directory.js';
|
|
32
21
|
import { printSyncResult, syncAgentDirectory } from '../lib/agent-sync.js';
|
|
33
22
|
import { getActor, recordAuditEvent } from '../lib/audit.js';
|
|
34
23
|
import { ALL_BUILTINS } from '../lib/builtin-agents.js';
|
|
35
24
|
import { contractPath } from '../lib/genie-config.js';
|
|
36
|
-
import { findOmniAgent, registerAgentInOmni, resolveOmniApiUrl } from '../lib/omni-registration.js';
|
|
37
25
|
|
|
38
26
|
export function registerDirNamespace(program: Command): void {
|
|
39
27
|
const dir = program.command('dir').description('Agent directory management');
|
|
@@ -66,9 +54,6 @@ export function registerDirNamespace(program: Command): void {
|
|
|
66
54
|
.action(async (name: string, options: { global?: boolean }) => {
|
|
67
55
|
try {
|
|
68
56
|
const removed = await directory.rm(name, { global: options.global });
|
|
69
|
-
// Also remove from app_store
|
|
70
|
-
await removeItemFromStore(name).catch(() => {});
|
|
71
|
-
await regenerateAgentCache();
|
|
72
57
|
recordAuditEvent('item', name, 'item_removed', getActor(), { type: 'agent', source: 'dir_rm' }).catch(() => {});
|
|
73
58
|
|
|
74
59
|
if (removed) {
|
|
@@ -165,18 +150,6 @@ async function handleDirAdd(name: string, options: DirAddOptions): Promise<void>
|
|
|
165
150
|
{ global: options.global },
|
|
166
151
|
);
|
|
167
152
|
|
|
168
|
-
// Also register in app_store (primary source of truth)
|
|
169
|
-
try {
|
|
170
|
-
await registerItemInStore({
|
|
171
|
-
name,
|
|
172
|
-
itemType: 'agent',
|
|
173
|
-
installPath: resolvedDir,
|
|
174
|
-
manifest: { promptMode, model: options.model, roles: normalizeRoles(options.roles), repo: options.repo },
|
|
175
|
-
});
|
|
176
|
-
} catch {
|
|
177
|
-
// Best-effort — legacy agents table is still the spawn path
|
|
178
|
-
}
|
|
179
|
-
await regenerateAgentCache();
|
|
180
153
|
recordAuditEvent('item', name, 'item_registered', getActor(), { type: 'agent', source: 'dir_add' }).catch(() => {});
|
|
181
154
|
|
|
182
155
|
const scope = options.global ? 'global' : 'project';
|
|
@@ -208,16 +181,6 @@ async function handleEdit(name: string, options: EditOptions): Promise<void> {
|
|
|
208
181
|
|
|
209
182
|
const entry = await directory.edit(name, updates, { global: options.global });
|
|
210
183
|
|
|
211
|
-
// Also update app_store
|
|
212
|
-
try {
|
|
213
|
-
await updateItemInStore(name, {
|
|
214
|
-
installPath: updates.dir,
|
|
215
|
-
manifest: { promptMode: updates.promptMode, model: updates.model, roles: updates.roles, repo: updates.repo },
|
|
216
|
-
});
|
|
217
|
-
} catch {
|
|
218
|
-
// Best-effort
|
|
219
|
-
}
|
|
220
|
-
await regenerateAgentCache();
|
|
221
184
|
recordAuditEvent('item', name, 'item_updated', getActor(), { type: 'agent', source: 'dir_edit' }).catch(() => {});
|
|
222
185
|
|
|
223
186
|
const scope = options.global ? 'global' : 'project';
|
|
@@ -280,37 +243,8 @@ async function showEntry(name: string, json?: boolean): Promise<void> {
|
|
|
280
243
|
console.log('');
|
|
281
244
|
}
|
|
282
245
|
|
|
283
|
-
async function listEntries(json?: boolean, includeBuiltins?: boolean,
|
|
284
|
-
|
|
285
|
-
await migrateAgentDirectory().catch(() => {});
|
|
286
|
-
|
|
287
|
-
// Try app_store first (primary), fall back to legacy agents table
|
|
288
|
-
let entries: directory.ScopedDirectoryEntry[];
|
|
289
|
-
try {
|
|
290
|
-
const storeItems = await listItemsFromStore('agent');
|
|
291
|
-
entries = storeItems
|
|
292
|
-
.filter((item: StoreRow) => {
|
|
293
|
-
if (includeArchived) return true;
|
|
294
|
-
const manifest = (item.manifest ?? {}) as Record<string, unknown>;
|
|
295
|
-
return !manifest.archived;
|
|
296
|
-
})
|
|
297
|
-
.map((item: StoreRow) => {
|
|
298
|
-
const manifest = (item.manifest ?? {}) as Record<string, unknown>;
|
|
299
|
-
return {
|
|
300
|
-
name: item.name,
|
|
301
|
-
dir: (item.install_path as string) ?? '',
|
|
302
|
-
repo: (manifest.repo as string) ?? '',
|
|
303
|
-
promptMode: ((manifest.promptMode as string) ?? 'append') as directory.PromptMode,
|
|
304
|
-
model: manifest.model as string | undefined,
|
|
305
|
-
roles: normalizeRoles(manifest.roles as string[] | undefined),
|
|
306
|
-
registeredAt: item.installed_at as string,
|
|
307
|
-
scope: (manifest.archived ? 'archived' : 'global') as directory.DirectoryScope,
|
|
308
|
-
};
|
|
309
|
-
});
|
|
310
|
-
} catch {
|
|
311
|
-
// Fallback to legacy
|
|
312
|
-
entries = await directory.ls();
|
|
313
|
-
}
|
|
246
|
+
async function listEntries(json?: boolean, includeBuiltins?: boolean, _includeArchived?: boolean): Promise<void> {
|
|
247
|
+
const entries = await directory.ls();
|
|
314
248
|
|
|
315
249
|
if (json) {
|
|
316
250
|
listEntriesJson(entries, includeBuiltins);
|
|
@@ -420,94 +354,3 @@ function printBuiltinsTable(): void {
|
|
|
420
354
|
}
|
|
421
355
|
console.log('');
|
|
422
356
|
}
|
|
423
|
-
|
|
424
|
-
// ============================================================================
|
|
425
|
-
// Agent namespace — genie agent register
|
|
426
|
-
// ============================================================================
|
|
427
|
-
|
|
428
|
-
interface RegisterOptions {
|
|
429
|
-
dir: string;
|
|
430
|
-
repo?: string;
|
|
431
|
-
promptMode: string;
|
|
432
|
-
model?: string;
|
|
433
|
-
roles?: string[];
|
|
434
|
-
global?: boolean;
|
|
435
|
-
skipOmni?: boolean;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
async function handleOmniRegistration(
|
|
439
|
-
name: string,
|
|
440
|
-
options: { model?: string; roles?: string[]; global?: boolean },
|
|
441
|
-
): Promise<void> {
|
|
442
|
-
const omniUrl = await resolveOmniApiUrl();
|
|
443
|
-
if (!omniUrl) return;
|
|
444
|
-
|
|
445
|
-
console.log(`\nRegistering in Omni (${omniUrl})...`);
|
|
446
|
-
|
|
447
|
-
const existingId = await findOmniAgent(name);
|
|
448
|
-
if (existingId) {
|
|
449
|
-
console.log(` Agent already exists in Omni: ${existingId}`);
|
|
450
|
-
await directory.edit(name, { omniAgentId: existingId }, { global: options.global });
|
|
451
|
-
console.log(' Linked existing Omni agent to directory entry.');
|
|
452
|
-
return;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
const omniAgentId = await registerAgentInOmni(name, {
|
|
456
|
-
model: options.model,
|
|
457
|
-
roles: options.roles,
|
|
458
|
-
});
|
|
459
|
-
if (omniAgentId) {
|
|
460
|
-
await directory.edit(name, { omniAgentId }, { global: options.global });
|
|
461
|
-
console.log(` Omni agent created: ${omniAgentId}`);
|
|
462
|
-
console.log(' Session isolation: per-person + per-channel');
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
async function handleAgentRegister(name: string, options: RegisterOptions): Promise<void> {
|
|
467
|
-
const promptMode = validatePromptMode(options.promptMode);
|
|
468
|
-
|
|
469
|
-
const roles = normalizeRoles(options.roles);
|
|
470
|
-
const entry = await directory.add(
|
|
471
|
-
{
|
|
472
|
-
name,
|
|
473
|
-
dir: resolvePath(options.dir),
|
|
474
|
-
repo: options.repo ? resolvePath(options.repo) : undefined,
|
|
475
|
-
promptMode,
|
|
476
|
-
model: options.model,
|
|
477
|
-
roles,
|
|
478
|
-
},
|
|
479
|
-
{ global: options.global },
|
|
480
|
-
);
|
|
481
|
-
|
|
482
|
-
const scope = options.global ? 'global' : 'project';
|
|
483
|
-
console.log(`Agent "${entry.name}" registered (${scope}).`);
|
|
484
|
-
printEntry(entry);
|
|
485
|
-
|
|
486
|
-
if (!options.skipOmni) {
|
|
487
|
-
await handleOmniRegistration(name, { ...options, roles });
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
export function registerAgentNamespace(program: Command): void {
|
|
492
|
-
const agent = program.command('agent').description('Agent lifecycle management');
|
|
493
|
-
|
|
494
|
-
agent
|
|
495
|
-
.command('register <name>')
|
|
496
|
-
.description('Register an agent locally and auto-register in Omni when configured')
|
|
497
|
-
.requiredOption('--dir <path>', 'Agent folder (CWD + AGENTS.md)')
|
|
498
|
-
.option('--repo <path>', 'Default git repo (overridden by team)')
|
|
499
|
-
.option('--prompt-mode <mode>', 'Prompt mode: append or system', 'append')
|
|
500
|
-
.option('--model <model>', 'Default model (sonnet, opus, codex)')
|
|
501
|
-
.option('--roles <roles...>', 'Built-in roles this agent can orchestrate')
|
|
502
|
-
.option('--global', 'Write to global directory instead of project')
|
|
503
|
-
.option('--skip-omni', 'Skip Omni auto-registration')
|
|
504
|
-
.action(async (name: string, options: RegisterOptions) => {
|
|
505
|
-
try {
|
|
506
|
-
await handleAgentRegister(name, options);
|
|
507
|
-
} catch (error) {
|
|
508
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
509
|
-
console.error(`Error: ${message}`);
|
|
510
|
-
process.exit(1);
|
|
511
|
-
}
|
|
512
|
-
});
|
|
513
|
-
}
|