@eve-horizon/cli 0.2.10 → 0.2.12

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 CHANGED
@@ -175,6 +175,32 @@ eve job approve MyProj-abc123 --comment "LGTM"
175
175
  eve job reject MyProj-abc123 --reason "Missing tests"
176
176
  ```
177
177
 
178
+ ### Agents
179
+
180
+ ```bash
181
+ # Inspect agent policy + harness readiness
182
+ eve agents config --json
183
+
184
+ # Sync agents/teams/chat config from repo (deterministic)
185
+ eve agents sync --project proj_xxx --ref main
186
+
187
+ # Local dev sync (requires local API + allow dirty)
188
+ eve agents sync --project proj_xxx --local --allow-dirty
189
+ ```
190
+
191
+ ### Integrations + Chat
192
+
193
+ ```bash
194
+ # Connect Slack workspace (stub OAuth)
195
+ eve integrations slack connect --org org_xxx --team-id T123 --token xoxb-...
196
+
197
+ # List integrations for org
198
+ eve integrations list --org org_xxx
199
+
200
+ # Simulate inbound Slack message
201
+ eve chat simulate --project proj_xxx --team-id T123 --channel-id C123 --user-id U123 --text "hello"
202
+ ```
203
+
178
204
  #### Job Results
179
205
 
180
206
  Fetch and display completed job results:
@@ -1,13 +1,16 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.handleAgents = handleAgents;
4
+ const node_child_process_1 = require("node:child_process");
4
5
  const node_fs_1 = require("node:fs");
5
6
  const node_path_1 = require("node:path");
6
7
  const yaml_1 = require("yaml");
8
+ const shared_1 = require("@eve/shared");
7
9
  const harness_capabilities_js_1 = require("../lib/harness-capabilities.js");
8
10
  const args_1 = require("../lib/args");
9
11
  const client_1 = require("../lib/client");
10
12
  const output_1 = require("../lib/output");
13
+ const git_js_1 = require("../lib/git.js");
11
14
  function readYamlFile(filePath) {
12
15
  const raw = (0, node_fs_1.readFileSync)(filePath, 'utf-8');
13
16
  const parsed = (0, yaml_1.parse)(raw);
@@ -16,6 +19,41 @@ function readYamlFile(filePath) {
16
19
  }
17
20
  return parsed;
18
21
  }
22
+ function resolveAgentsConfigPaths(repoRoot, manifest) {
23
+ const xEve = manifest['x-eve'] ||
24
+ manifest['x_eve'] ||
25
+ {};
26
+ const agentsBlock = xEve['agents'] || {};
27
+ const chatBlock = xEve['chat'] || {};
28
+ const manifestChat = manifest['chat'] || {};
29
+ const agentsPath = (0, node_path_1.resolve)(repoRoot, pickString(agentsBlock.config_path) ?? 'agents/agents.yaml');
30
+ const teamsPath = (0, node_path_1.resolve)(repoRoot, pickString(agentsBlock.teams_path) ?? 'agents/teams.yaml');
31
+ const chatPath = (0, node_path_1.resolve)(repoRoot, pickString(chatBlock.config_path) ?? pickString(manifestChat.config_path) ?? 'agents/chat.yaml');
32
+ return { agentsPath, teamsPath, chatPath };
33
+ }
34
+ function pickString(value) {
35
+ return typeof value === 'string' && value.trim().length > 0 ? value : undefined;
36
+ }
37
+ function ensureFileExists(path, label) {
38
+ if (!(0, node_fs_1.existsSync)(path)) {
39
+ throw new Error(`Missing ${label} at ${path}. Update manifest config_path or add the file.`);
40
+ }
41
+ return path;
42
+ }
43
+ function isLocalApiUrl(apiUrl) {
44
+ try {
45
+ const url = new URL(apiUrl);
46
+ const host = url.hostname.toLowerCase();
47
+ return (host === 'localhost' ||
48
+ host === '127.0.0.1' ||
49
+ host === '0.0.0.0' ||
50
+ host.endsWith('.lvh.me') ||
51
+ host.endsWith('.localhost'));
52
+ }
53
+ catch {
54
+ return false;
55
+ }
56
+ }
19
57
  function loadAgentsConfig(repoRoot) {
20
58
  const eveDir = (0, node_path_1.join)(repoRoot, '.eve');
21
59
  const manifestPath = (0, node_path_1.join)(eveDir, 'manifest.yaml');
@@ -30,49 +68,378 @@ function loadAgentsConfig(repoRoot) {
30
68
  }
31
69
  return { source: { type: 'none' }, policy: null };
32
70
  }
71
+ async function resolvePacksAndMerge(repoRoot, manifest, projectSlug) {
72
+ const xEve = (manifest['x-eve'] ?? manifest['x_eve']);
73
+ const packs = (xEve?.packs ?? []);
74
+ if (packs.length === 0)
75
+ return null;
76
+ console.log(`Resolving ${packs.length} pack(s)...`);
77
+ // 1. Resolve all packs
78
+ const resolvedPacks = [];
79
+ for (const entry of packs) {
80
+ const resolved = await (0, shared_1.resolvePack)(entry, projectSlug, repoRoot);
81
+ resolvedPacks.push(resolved);
82
+ console.log(` ✓ ${resolved.id} (${resolved.skillPaths.length} skills)`);
83
+ }
84
+ // 2. Check for agent ID collisions across packs
85
+ const seenAgentIds = new Map();
86
+ for (const pack of resolvedPacks) {
87
+ for (const agentId of Object.keys(pack.agents)) {
88
+ if (seenAgentIds.has(agentId)) {
89
+ throw new Error(`Agent ID collision: "${agentId}" defined in both pack "${seenAgentIds.get(agentId)}" and "${pack.id}". ` +
90
+ `Agent IDs must be unique across all packs.`);
91
+ }
92
+ seenAgentIds.set(agentId, pack.id);
93
+ }
94
+ }
95
+ // 3. Merge pack bases in listed order
96
+ let mergedAgents = {};
97
+ let mergedTeams = {};
98
+ let mergedChat = { routes: [] };
99
+ const xEveFragments = [];
100
+ for (const pack of resolvedPacks) {
101
+ mergedAgents = (0, shared_1.mergeMapConfig)(mergedAgents, pack.agents);
102
+ mergedTeams = (0, shared_1.mergeMapConfig)(mergedTeams, pack.teams);
103
+ if (pack.chat) {
104
+ mergedChat = (0, shared_1.mergeChatConfig)(mergedChat, pack.chat);
105
+ }
106
+ if (pack.xEve) {
107
+ xEveFragments.push(pack.xEve);
108
+ }
109
+ }
110
+ // 4. Load project overlay files and merge on top
111
+ const configPaths = resolveAgentsConfigPaths(repoRoot, manifest);
112
+ if ((0, node_fs_1.existsSync)(configPaths.agentsPath)) {
113
+ const projectAgents = (0, yaml_1.parse)((0, node_fs_1.readFileSync)(configPaths.agentsPath, 'utf-8')) ?? {};
114
+ mergedAgents = (0, shared_1.mergeMapConfig)(mergedAgents, projectAgents);
115
+ }
116
+ if ((0, node_fs_1.existsSync)(configPaths.teamsPath)) {
117
+ const projectTeams = (0, yaml_1.parse)((0, node_fs_1.readFileSync)(configPaths.teamsPath, 'utf-8')) ?? {};
118
+ mergedTeams = (0, shared_1.mergeMapConfig)(mergedTeams, projectTeams);
119
+ }
120
+ if ((0, node_fs_1.existsSync)(configPaths.chatPath)) {
121
+ const projectChat = (0, yaml_1.parse)((0, node_fs_1.readFileSync)(configPaths.chatPath, 'utf-8')) ?? {};
122
+ mergedChat = (0, shared_1.mergeChatConfig)(mergedChat, projectChat);
123
+ }
124
+ // 5. Merge x-eve (strip packs + install_agents from project overlay)
125
+ const projectXEve = (xEve ?? {});
126
+ const { packs: _p, install_agents: _ia, ...projectXEveRest } = projectXEve;
127
+ (0, shared_1.mergeXEve)(xEveFragments, projectXEveRest);
128
+ // 6. Validate effective config
129
+ validateEffectiveConfig(mergedAgents, mergedTeams, mergedChat);
130
+ // 7. Write lockfile
131
+ const lockfile = {
132
+ resolved_at: new Date().toISOString(),
133
+ project_slug: projectSlug,
134
+ packs: resolvedPacks.map((p) => ({
135
+ id: p.id,
136
+ source: p.source,
137
+ ref: p.ref,
138
+ pack_version: 1,
139
+ })),
140
+ effective: {
141
+ agents_count: Object.keys((0, shared_1.extractInnerMap)(mergedAgents, 'agents')).length,
142
+ teams_count: Object.keys((0, shared_1.extractInnerMap)(mergedTeams, 'teams')).length,
143
+ routes_count: (mergedChat.routes ?? []).length,
144
+ profiles_count: 0,
145
+ agents_hash: simpleHash(JSON.stringify(mergedAgents)),
146
+ teams_hash: simpleHash(JSON.stringify(mergedTeams)),
147
+ chat_hash: simpleHash(JSON.stringify(mergedChat)),
148
+ },
149
+ };
150
+ const eveDir = (0, node_path_1.join)(repoRoot, '.eve');
151
+ (0, node_fs_1.mkdirSync)(eveDir, { recursive: true });
152
+ const lockfilePath = (0, node_path_1.join)(eveDir, 'packs.lock.yaml');
153
+ (0, node_fs_1.writeFileSync)(lockfilePath, (0, yaml_1.stringify)(lockfile), 'utf-8');
154
+ console.log(` ✓ Lockfile written: .eve/packs.lock.yaml`);
155
+ // 8. Return effective config as YAML strings
156
+ const packRefs = resolvedPacks.map((p) => ({ id: p.id, source: p.source, ref: p.ref }));
157
+ return {
158
+ agentsYaml: (0, yaml_1.stringify)(mergedAgents),
159
+ teamsYaml: (0, yaml_1.stringify)(mergedTeams),
160
+ chatYaml: (0, yaml_1.stringify)(mergedChat),
161
+ packRefs,
162
+ };
163
+ }
164
+ function validateEffectiveConfig(agents, teams, chat) {
165
+ // Extract inner maps (handles both nested `{ version: 1, agents: { ... } }` and flat formats)
166
+ const agentsMap = (0, shared_1.extractInnerMap)(agents, 'agents');
167
+ const teamsMap = (0, shared_1.extractInnerMap)(teams, 'teams');
168
+ // Check slug uniqueness
169
+ const slugs = new Set();
170
+ for (const [, entry] of Object.entries(agentsMap)) {
171
+ if (typeof entry !== 'object' || entry === null)
172
+ continue;
173
+ const agent = entry;
174
+ const slug = agent.slug;
175
+ if (slug) {
176
+ if (slugs.has(slug)) {
177
+ throw new Error(`Duplicate agent slug "${slug}" in effective config. Slugs must be unique.`);
178
+ }
179
+ slugs.add(slug);
180
+ }
181
+ }
182
+ // Check team references
183
+ const agentIds = new Set(Object.keys(agentsMap));
184
+ for (const [teamId, entry] of Object.entries(teamsMap)) {
185
+ if (typeof entry !== 'object' || entry === null)
186
+ continue;
187
+ const team = entry;
188
+ const lead = team.lead;
189
+ if (lead && !agentIds.has(lead)) {
190
+ throw new Error(`Team "${teamId}" references unknown agent "${lead}" as lead.`);
191
+ }
192
+ const members = (team.members ?? []);
193
+ for (const member of members) {
194
+ if (!agentIds.has(member)) {
195
+ throw new Error(`Team "${teamId}" references unknown agent "${member}" as member.`);
196
+ }
197
+ }
198
+ }
199
+ // Check route targets (supports "team:name" and "agent:name" prefixes)
200
+ const teamIds = new Set(Object.keys(teamsMap));
201
+ const routes = (chat.routes ?? []);
202
+ for (const route of routes) {
203
+ if (!route.target)
204
+ continue;
205
+ let targetId = route.target;
206
+ if (targetId.startsWith('team:')) {
207
+ targetId = targetId.slice(5);
208
+ if (!teamIds.has(targetId)) {
209
+ throw new Error(`Route "${route.id}" targets unknown team "${route.target}".`);
210
+ }
211
+ }
212
+ else if (targetId.startsWith('agent:')) {
213
+ targetId = targetId.slice(6);
214
+ if (!agentIds.has(targetId)) {
215
+ throw new Error(`Route "${route.id}" targets unknown agent "${route.target}".`);
216
+ }
217
+ }
218
+ else if (!agentIds.has(targetId) && !teamIds.has(targetId)) {
219
+ throw new Error(`Route "${route.id}" targets unknown agent/team "${route.target}".`);
220
+ }
221
+ }
222
+ }
223
+ function simpleHash(input) {
224
+ let hash = 0;
225
+ for (let i = 0; i < input.length; i++) {
226
+ const char = input.charCodeAt(i);
227
+ hash = ((hash << 5) - hash) + char;
228
+ hash = hash & hash; // Convert to 32bit integer
229
+ }
230
+ return Math.abs(hash).toString(16).padStart(8, '0');
231
+ }
33
232
  async function handleAgents(subcommand, positionals, flags, context) {
34
233
  const command = subcommand ?? 'config';
35
234
  const json = Boolean(flags.json);
36
235
  const includeHarnesses = !((0, args_1.getBooleanFlag)(flags, ['no-harnesses']) ?? false);
37
- const repoRoot = (0, node_path_1.resolve)((0, args_1.getStringFlag)(flags, ['path']) ?? process.cwd());
38
- if (command !== 'config') {
39
- throw new Error('Usage: eve agents config [--path <dir>] [--no-harnesses]');
40
- }
41
- const result = loadAgentsConfig(repoRoot);
42
- const response = {
43
- repo_root: repoRoot,
44
- source: result.source,
45
- policy: result.policy,
46
- };
47
- if (result.manifest_defaults) {
48
- response.manifest_defaults = result.manifest_defaults;
49
- }
50
- if (includeHarnesses) {
51
- const harnesses = await (0, client_1.requestJson)(context, '/harnesses');
52
- response.harnesses = harnesses.data;
53
- response.capabilities = harness_capabilities_js_1.HARNESS_CAPABILITIES;
236
+ const repoRoot = (0, node_path_1.resolve)((0, args_1.getStringFlag)(flags, ['repo-dir', 'repo_dir', 'dir', 'path']) ?? process.cwd());
237
+ switch (command) {
238
+ case 'config': {
239
+ const result = loadAgentsConfig(repoRoot);
240
+ const response = {
241
+ repo_root: repoRoot,
242
+ source: result.source,
243
+ policy: result.policy,
244
+ };
245
+ if (result.manifest_defaults) {
246
+ response.manifest_defaults = result.manifest_defaults;
247
+ }
248
+ if (includeHarnesses) {
249
+ const harnesses = await (0, client_1.requestJson)(context, '/harnesses');
250
+ response.harnesses = harnesses.data;
251
+ response.capabilities = harness_capabilities_js_1.HARNESS_CAPABILITIES;
252
+ }
253
+ if (json) {
254
+ (0, output_1.outputJson)(response, json);
255
+ return;
256
+ }
257
+ console.log(`Agents config source: ${result.source.type}`);
258
+ if ('path' in result.source) {
259
+ console.log(`Path: ${result.source.path}`);
260
+ }
261
+ if (!result.policy) {
262
+ console.log('No policy found. Add x-eve.agents to .eve/manifest.yaml.');
263
+ }
264
+ else {
265
+ const profiles = result.policy.profiles || {};
266
+ const profileNames = Object.keys(profiles);
267
+ console.log(`Profiles: ${profileNames.length ? profileNames.join(', ') : 'none'}`);
268
+ }
269
+ if (includeHarnesses) {
270
+ const harnesses = response.harnesses;
271
+ if (harnesses?.length) {
272
+ const ready = harnesses.filter((h) => h.auth.available).length;
273
+ console.log(`Harnesses: ${harnesses.length} (${ready} ready)`);
274
+ }
275
+ }
276
+ return;
277
+ }
278
+ case 'sync': {
279
+ const projectId = (0, args_1.getStringFlag)(flags, ['project']) ?? context.projectId;
280
+ if (!projectId) {
281
+ throw new Error('Missing project id. Provide --project or set a profile default.');
282
+ }
283
+ const ref = (0, args_1.getStringFlag)(flags, ['ref']);
284
+ const local = (0, args_1.getBooleanFlag)(flags, ['local']) ?? false;
285
+ const allowDirty = (0, args_1.getBooleanFlag)(flags, ['allow-dirty', 'allow_dirty']) ?? false;
286
+ const forceNonlocal = (0, args_1.getBooleanFlag)(flags, ['force-nonlocal', 'force_nonlocal']) ?? false;
287
+ if (local && ref) {
288
+ throw new Error('Use either --local or --ref, not both.');
289
+ }
290
+ if (!local && !ref) {
291
+ throw new Error('Missing --ref. Use --ref <sha|branch> or --local for dev sync.');
292
+ }
293
+ if (local && !forceNonlocal && !isLocalApiUrl(context.apiUrl)) {
294
+ throw new Error(`--local sync is only allowed for local API URLs (localhost or *.lvh.me). ` +
295
+ `Current API: ${context.apiUrl}. Use --force-nonlocal to override.`);
296
+ }
297
+ const gitRoot = (0, git_js_1.getGitRoot)(repoRoot);
298
+ if (!gitRoot) {
299
+ throw new Error('Not a git repository. Run from the repo root or pass --repo-dir <path>.');
300
+ }
301
+ const dirty = (0, git_js_1.isGitDirty)(gitRoot);
302
+ if (dirty && !allowDirty) {
303
+ throw new Error('Working tree is dirty. Commit changes or pass --allow-dirty to sync anyway.');
304
+ }
305
+ const manifestPath = (0, node_path_1.join)(repoRoot, '.eve', 'manifest.yaml');
306
+ if (!(0, node_fs_1.existsSync)(manifestPath)) {
307
+ throw new Error(`Missing manifest at ${manifestPath}. Expected .eve/manifest.yaml.`);
308
+ }
309
+ const manifest = readYamlFile(manifestPath);
310
+ // Pack resolution: if packs exist, resolve and merge before posting
311
+ const projectSlug = manifest.project ?? projectId.replace(/^proj_/, '');
312
+ const packResult = await resolvePacksAndMerge(repoRoot, manifest, projectSlug);
313
+ let agentsYaml;
314
+ let teamsYaml;
315
+ let chatYaml;
316
+ let packRefs;
317
+ if (packResult) {
318
+ agentsYaml = packResult.agentsYaml;
319
+ teamsYaml = packResult.teamsYaml;
320
+ chatYaml = packResult.chatYaml;
321
+ packRefs = packResult.packRefs;
322
+ }
323
+ else {
324
+ const configPaths = resolveAgentsConfigPaths(repoRoot, manifest);
325
+ agentsYaml = (0, node_fs_1.readFileSync)(ensureFileExists(configPaths.agentsPath, 'agents config'), 'utf-8');
326
+ teamsYaml = (0, node_fs_1.readFileSync)(ensureFileExists(configPaths.teamsPath, 'teams config'), 'utf-8');
327
+ chatYaml = (0, node_fs_1.readFileSync)(ensureFileExists(configPaths.chatPath, 'chat config'), 'utf-8');
328
+ }
329
+ let gitSha;
330
+ let branch;
331
+ let gitRef = ref ?? 'local';
332
+ if (ref) {
333
+ gitSha = await (0, git_js_1.resolveGitRef)(context, projectId, ref, repoRoot);
334
+ branch = (0, git_js_1.resolveGitBranch)(gitRoot, ref) ?? undefined;
335
+ }
336
+ else {
337
+ gitSha = (0, node_child_process_1.execSync)('git rev-parse HEAD', { cwd: gitRoot, encoding: 'utf-8' }).trim();
338
+ branch = (0, git_js_1.getGitBranch)(gitRoot) ?? undefined;
339
+ }
340
+ if (dirty) {
341
+ gitRef = `dirty:${gitRef}`;
342
+ }
343
+ const response = await (0, client_1.requestJson)(context, `/projects/${projectId}/agents/sync`, {
344
+ method: 'POST',
345
+ body: {
346
+ agents_yaml: agentsYaml,
347
+ teams_yaml: teamsYaml,
348
+ chat_yaml: chatYaml,
349
+ git_sha: gitSha,
350
+ branch,
351
+ git_ref: gitRef,
352
+ pack_refs: packRefs,
353
+ },
354
+ });
355
+ if (json) {
356
+ (0, output_1.outputJson)(response, json);
357
+ return;
358
+ }
359
+ console.log(`✓ Agents config synced to ${projectId}`);
360
+ if (packRefs) {
361
+ console.log(` Packs: ${packRefs.map((p) => p.id).join(', ')}`);
362
+ console.log(` Source: merged (packs + project overlay)`);
363
+ }
364
+ else {
365
+ const configPaths = resolveAgentsConfigPaths(repoRoot, manifest);
366
+ console.log(` Agents: ${configPaths.agentsPath}`);
367
+ console.log(` Teams: ${configPaths.teamsPath}`);
368
+ console.log(` Chat: ${configPaths.chatPath}`);
369
+ }
370
+ if (gitSha)
371
+ console.log(` Git SHA: ${gitSha.substring(0, 8)}`);
372
+ if (branch)
373
+ console.log(` Branch: ${branch}`);
374
+ if (dirty)
375
+ console.log(' Warning: working tree dirty — marked non-deployable');
376
+ return;
377
+ }
378
+ case 'runtime-status': {
379
+ const orgId = (0, args_1.getStringFlag)(flags, ['org']) ?? context.orgId;
380
+ if (!orgId) {
381
+ throw new Error('Missing org id. Provide --org or set a profile default.');
382
+ }
383
+ const response = await (0, client_1.requestJson)(context, `/orgs/${orgId}/agent-runtime/status`);
384
+ if (json) {
385
+ (0, output_1.outputJson)(response, json);
386
+ return;
387
+ }
388
+ formatAgentRuntimeStatus(response, orgId);
389
+ return;
390
+ }
391
+ default:
392
+ throw new Error('Usage: eve agents <config|sync|runtime-status>');
54
393
  }
55
- if (json) {
56
- (0, output_1.outputJson)(response, json);
394
+ }
395
+ function formatAgentRuntimeStatus(response, orgId) {
396
+ console.log(`Agent Runtime Status: ${orgId}`);
397
+ if (response.pods.length === 0) {
398
+ console.log('');
399
+ console.log('No agent runtime pods found.');
57
400
  return;
58
401
  }
59
- console.log(`Agents config source: ${result.source.type}`);
60
- if ('path' in result.source) {
61
- console.log(`Path: ${result.source.path}`);
402
+ const nameWidth = Math.max(7, ...response.pods.map((pod) => pod.pod_name.length));
403
+ const statusWidth = Math.max(6, ...response.pods.map((pod) => pod.status.length));
404
+ const capacityWidth = Math.max(8, ...response.pods.map((pod) => String(pod.capacity).length));
405
+ const ageWidth = Math.max(3, ...response.pods.map((pod) => formatAgeSeconds(pod.last_heartbeat_at).length));
406
+ console.log('');
407
+ const header = [
408
+ padRight('Pod', nameWidth),
409
+ padRight('Status', statusWidth),
410
+ padRight('Capacity', capacityWidth),
411
+ padRight('Age', ageWidth),
412
+ 'Last Heartbeat',
413
+ ].join(' ');
414
+ console.log(header);
415
+ console.log('-'.repeat(header.length));
416
+ for (const pod of response.pods) {
417
+ const age = formatAgeSeconds(pod.last_heartbeat_at);
418
+ console.log([
419
+ padRight(pod.pod_name, nameWidth),
420
+ padRight(pod.status, statusWidth),
421
+ padRight(String(pod.capacity), capacityWidth),
422
+ padRight(age, ageWidth),
423
+ pod.last_heartbeat_at,
424
+ ].join(' '));
62
425
  }
63
- if (!result.policy) {
64
- console.log('No policy found. Add x-eve.agents to .eve/manifest.yaml.');
426
+ }
427
+ function padRight(value, width) {
428
+ return value.length >= width ? value : `${value}${' '.repeat(width - value.length)}`;
429
+ }
430
+ function formatAgeSeconds(isoDate) {
431
+ const timestamp = Date.parse(isoDate);
432
+ if (!Number.isFinite(timestamp)) {
433
+ return '-';
65
434
  }
66
- else {
67
- const profiles = result.policy.profiles || {};
68
- const profileNames = Object.keys(profiles);
69
- console.log(`Profiles: ${profileNames.length ? profileNames.join(', ') : 'none'}`);
435
+ const diffSeconds = Math.max(0, Math.floor((Date.now() - timestamp) / 1000));
436
+ if (diffSeconds < 60) {
437
+ return `${diffSeconds}s`;
70
438
  }
71
- if (includeHarnesses) {
72
- const harnesses = response.harnesses;
73
- if (harnesses?.length) {
74
- const ready = harnesses.filter((h) => h.auth.available).length;
75
- console.log(`Harnesses: ${harnesses.length} (${ready} ready)`);
76
- }
439
+ const diffMinutes = Math.floor(diffSeconds / 60);
440
+ if (diffMinutes < 60) {
441
+ return `${diffMinutes}m`;
77
442
  }
443
+ const diffHours = Math.floor(diffMinutes / 60);
444
+ return `${diffHours}h`;
78
445
  }
@@ -174,9 +174,38 @@ async function handleAuth(subcommand, flags, context, credentials) {
174
174
  (0, output_1.outputJson)(data, json, 'Auth disabled for this stack.');
175
175
  return;
176
176
  }
177
+ if (!json && data.permissions && data.permissions.length > 0) {
178
+ console.log(`User: ${data.user_id ?? 'unknown'} (${data.email ?? 'no email'})`);
179
+ console.log(`Role: ${data.role ?? 'member'}`);
180
+ console.log(`Admin: ${data.is_admin ?? false}`);
181
+ console.log(`\nPermissions (${data.permissions.length}):`);
182
+ for (const perm of data.permissions) {
183
+ console.log(` ${perm}`);
184
+ }
185
+ return;
186
+ }
177
187
  (0, output_1.outputJson)(data, json);
178
188
  return;
179
189
  }
190
+ case 'permissions': {
191
+ const response = await (0, client_1.requestJson)(context, '/auth/permissions');
192
+ if (json) {
193
+ (0, output_1.outputJson)(response, json);
194
+ return;
195
+ }
196
+ console.log('Permission Matrix:');
197
+ console.log('');
198
+ const header = 'Permission'.padEnd(24) + 'Member'.padEnd(10) + 'Admin'.padEnd(10) + 'Owner';
199
+ console.log(header);
200
+ console.log('-'.repeat(header.length));
201
+ for (const row of response.matrix) {
202
+ const m = row.member ? '✓' : '-';
203
+ const a = row.admin ? '✓' : '-';
204
+ const o = row.owner ? '✓' : '-';
205
+ console.log(`${row.permission.padEnd(24)}${m.padEnd(10)}${a.padEnd(10)}${o}`);
206
+ }
207
+ return;
208
+ }
180
209
  case 'bootstrap': {
181
210
  const statusOnly = (0, args_1.getBooleanFlag)(flags, ['status']) ?? false;
182
211
  // Check bootstrap status first
@@ -681,7 +710,7 @@ async function handleAuth(subcommand, flags, context, credentials) {
681
710
  return;
682
711
  }
683
712
  default:
684
- throw new Error('Usage: eve auth <login|logout|status|whoami|bootstrap|sync|creds|token>');
713
+ throw new Error('Usage: eve auth <login|logout|status|whoami|bootstrap|sync|creds|token|mint|permissions>');
685
714
  }
686
715
  }
687
716
  function signNonceWithSsh(keyPath, nonce) {
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.handleChat = handleChat;
4
+ const args_1 = require("../lib/args");
5
+ const client_1 = require("../lib/client");
6
+ const output_1 = require("../lib/output");
7
+ async function handleChat(subcommand, _positionals, flags, context) {
8
+ const json = Boolean(flags.json);
9
+ switch (subcommand) {
10
+ case 'simulate': {
11
+ const projectId = (0, args_1.getStringFlag)(flags, ['project']) ?? context.projectId;
12
+ if (!projectId) {
13
+ throw new Error('Missing project id. Provide --project or set a profile default.');
14
+ }
15
+ const provider = (0, args_1.getStringFlag)(flags, ['provider']) ?? 'slack';
16
+ const teamId = (0, args_1.getStringFlag)(flags, ['team-id']);
17
+ const text = (0, args_1.getStringFlag)(flags, ['text']);
18
+ if (!teamId || !text) {
19
+ throw new Error('Usage: eve chat simulate --project <id> --team-id <team> --text <message>');
20
+ }
21
+ const channelId = (0, args_1.getStringFlag)(flags, ['channel-id']);
22
+ const userId = (0, args_1.getStringFlag)(flags, ['user-id']);
23
+ const threadKey = (0, args_1.getStringFlag)(flags, ['thread-key']);
24
+ const metadataFlag = (0, args_1.getStringFlag)(flags, ['metadata']);
25
+ let metadata;
26
+ if (metadataFlag) {
27
+ try {
28
+ const parsed = JSON.parse(metadataFlag);
29
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
30
+ metadata = parsed;
31
+ }
32
+ }
33
+ catch {
34
+ throw new Error('Invalid --metadata (must be JSON object)');
35
+ }
36
+ }
37
+ const response = await (0, client_1.requestJson)(context, `/projects/${projectId}/chat/simulate`, {
38
+ method: 'POST',
39
+ body: {
40
+ provider,
41
+ team_id: teamId,
42
+ channel_id: channelId,
43
+ user_id: userId,
44
+ text,
45
+ thread_key: threadKey,
46
+ metadata,
47
+ },
48
+ });
49
+ (0, output_1.outputJson)(response, json, `✓ Chat simulated (thread: ${response.thread_id})`);
50
+ return;
51
+ }
52
+ default:
53
+ throw new Error('Usage: eve chat <simulate>');
54
+ }
55
+ }