@phnx-labs/agents-cli 1.20.90 → 1.20.92
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/CHANGELOG.md +276 -0
- package/README.md +1 -1
- package/dist/bin/agents +0 -0
- package/dist/commands/activity.d.ts +72 -6
- package/dist/commands/activity.js +198 -49
- package/dist/commands/beta.js +1 -0
- package/dist/commands/doctor.js +4 -2
- package/dist/commands/exec.d.ts +14 -0
- package/dist/commands/exec.js +144 -14
- package/dist/commands/feed.js +77 -4
- package/dist/commands/hooks.js +22 -6
- package/dist/commands/perf.d.ts +14 -0
- package/dist/commands/perf.js +221 -0
- package/dist/commands/projects.d.ts +12 -0
- package/dist/commands/projects.js +358 -0
- package/dist/commands/routines.js +30 -24
- package/dist/commands/secrets.d.ts +43 -4
- package/dist/commands/secrets.js +217 -32
- package/dist/commands/send.d.ts +5 -1
- package/dist/commands/send.js +1 -1
- package/dist/commands/sessions-picker.d.ts +15 -0
- package/dist/commands/sessions-picker.js +107 -13
- package/dist/commands/sessions-resume.d.ts +2 -0
- package/dist/commands/sessions-resume.js +9 -1
- package/dist/commands/sessions.d.ts +10 -5
- package/dist/commands/sessions.js +65 -27
- package/dist/index.js +19 -3
- package/dist/lib/activity.d.ts +80 -13
- package/dist/lib/activity.js +418 -74
- package/dist/lib/beta.d.ts +1 -1
- package/dist/lib/beta.js +1 -1
- package/dist/lib/catchup.d.ts +105 -0
- package/dist/lib/catchup.js +160 -0
- package/dist/lib/channels/providers/desktop.d.ts +49 -0
- package/dist/lib/channels/providers/desktop.js +132 -0
- package/dist/lib/channels/providers/index.js +2 -0
- package/dist/lib/daemon.js +74 -13
- package/dist/lib/devices/registry.d.ts +14 -0
- package/dist/lib/devices/registry.js +37 -0
- package/dist/lib/events.d.ts +12 -0
- package/dist/lib/events.js +122 -9
- package/dist/lib/exec.js +10 -0
- package/dist/lib/feed-broadcast.d.ts +47 -0
- package/dist/lib/feed-broadcast.js +65 -1
- package/dist/lib/feed-post.d.ts +10 -0
- package/dist/lib/feed-post.js +9 -3
- package/dist/lib/feed.d.ts +47 -1
- package/dist/lib/feed.js +38 -0
- package/dist/lib/hooks/cache.d.ts +2 -0
- package/dist/lib/hooks/cache.js +24 -4
- package/dist/lib/hosts/remote-cmd.js +4 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +14 -4
- package/dist/lib/menubar/install-menubar.js +20 -6
- package/dist/lib/overdue.d.ts +14 -0
- package/dist/lib/overdue.js +37 -1
- package/dist/lib/perf/db.d.ts +25 -0
- package/dist/lib/perf/db.js +290 -0
- package/dist/lib/perf/spool.d.ts +18 -0
- package/dist/lib/perf/spool.js +79 -0
- package/dist/lib/perf/types.d.ts +45 -0
- package/dist/lib/perf/types.js +2 -0
- package/dist/lib/project-key.d.ts +44 -0
- package/dist/lib/project-key.js +79 -0
- package/dist/lib/project-root.js +16 -0
- package/dist/lib/project-status.d.ts +69 -0
- package/dist/lib/project-status.js +101 -0
- package/dist/lib/projects.d.ts +138 -0
- package/dist/lib/projects.js +301 -0
- package/dist/lib/remote-agents-json.d.ts +9 -0
- package/dist/lib/remote-agents-json.js +11 -5
- package/dist/lib/routines-project.js +6 -0
- package/dist/lib/routines.d.ts +30 -1
- package/dist/lib/routines.js +11 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/list-filter.d.ts +94 -0
- package/dist/lib/secrets/list-filter.js +245 -0
- package/dist/lib/session/bash-command.d.ts +53 -0
- package/dist/lib/session/bash-command.js +364 -0
- package/dist/lib/session/digest.d.ts +13 -0
- package/dist/lib/session/digest.js +48 -1
- package/dist/lib/session/discover.d.ts +1 -2
- package/dist/lib/session/discover.js +7 -24
- package/dist/lib/session/highlights.d.ts +82 -0
- package/dist/lib/session/highlights.js +251 -0
- package/dist/lib/session/parse.js +23 -1
- package/dist/lib/session/relative-time.d.ts +37 -0
- package/dist/lib/session/relative-time.js +96 -8
- package/dist/lib/session/remote-list.js +5 -2
- package/dist/lib/session/render.d.ts +9 -9
- package/dist/lib/session/render.js +112 -73
- package/dist/lib/session/types.d.ts +4 -1
- package/dist/lib/ssh-exec.d.ts +6 -0
- package/dist/lib/ssh-exec.js +10 -1
- package/dist/lib/startup/command-registry.d.ts +2 -0
- package/dist/lib/startup/command-registry.js +4 -0
- package/dist/lib/state.d.ts +11 -0
- package/dist/lib/state.js +16 -0
- package/dist/lib/terminal/backends/index.d.ts +10 -2
- package/dist/lib/terminal/backends/index.js +14 -2
- package/dist/lib/terminal/backends/terminal-app.d.ts +13 -0
- package/dist/lib/terminal/backends/terminal-app.js +73 -0
- package/dist/lib/terminal/index.d.ts +2 -1
- package/dist/lib/terminal/index.js +2 -1
- package/dist/lib/terminal/preferred.d.ts +89 -0
- package/dist/lib/terminal/preferred.js +87 -0
- package/dist/lib/terminal/run-surface.d.ts +82 -0
- package/dist/lib/terminal/run-surface.js +146 -0
- package/dist/lib/terminal/types.d.ts +1 -1
- package/dist/lib/types.d.ts +1 -1
- package/package.json +4 -1
package/dist/lib/activity.js
CHANGED
|
@@ -24,6 +24,7 @@ import chalk from 'chalk';
|
|
|
24
24
|
import { relTime, truncate } from './format.js';
|
|
25
25
|
import { getActivityDir, getUserAgentsDir } from './state.js';
|
|
26
26
|
import { normalizeHost } from './machine-id.js';
|
|
27
|
+
import { projectKeyFromCwd } from './project-key.js';
|
|
27
28
|
/** The set of milestone events, for tier classification and ordering. */
|
|
28
29
|
export const MILESTONE_EVENTS = [
|
|
29
30
|
'plan.created',
|
|
@@ -37,7 +38,12 @@ export const MILESTONE_EVENTS = [
|
|
|
37
38
|
'artifact.created',
|
|
38
39
|
'task.completed',
|
|
39
40
|
'checklist.created',
|
|
41
|
+
'video.rendered',
|
|
42
|
+
'video.converted',
|
|
43
|
+
'image.upscaled',
|
|
44
|
+
'metadata.edited',
|
|
40
45
|
'status.posted',
|
|
46
|
+
'status.blocked',
|
|
41
47
|
];
|
|
42
48
|
const MILESTONE_SET = new Set(MILESTONE_EVENTS);
|
|
43
49
|
export function tierForEvent(event) {
|
|
@@ -130,6 +136,9 @@ function parseLine(line) {
|
|
|
130
136
|
launchId: parsed.launchId,
|
|
131
137
|
terminalId: parsed.terminalId,
|
|
132
138
|
tmuxPane: parsed.tmuxPane,
|
|
139
|
+
category: typeof parsed.category === 'string' ? parsed.category : undefined,
|
|
140
|
+
bashTool: typeof parsed.bashTool === 'string' ? parsed.bashTool : undefined,
|
|
141
|
+
bashAction: typeof parsed.bashAction === 'string' ? parsed.bashAction : undefined,
|
|
133
142
|
attachments: sanitizeAttachments(parsed.attachments),
|
|
134
143
|
};
|
|
135
144
|
}
|
|
@@ -293,6 +302,7 @@ export const EVENT_STYLE = {
|
|
|
293
302
|
'checklist.created': { glyph: '☐', color: chalk.cyan, label: 'checklist created' },
|
|
294
303
|
'status.posted': { glyph: '▸', color: chalk.white, label: 'status' },
|
|
295
304
|
'file.edited': { glyph: '·', color: chalk.gray, label: 'file edited' },
|
|
305
|
+
'bash.executed': { glyph: '$', color: chalk.gray, label: 'command run' },
|
|
296
306
|
};
|
|
297
307
|
export function styleForEvent(event) {
|
|
298
308
|
return EVENT_STYLE[event] ?? { glyph: '•', color: chalk.white, label: event };
|
|
@@ -385,41 +395,38 @@ export function formatProgressUpdate(ev, opts = {}) {
|
|
|
385
395
|
return lines.join('\n');
|
|
386
396
|
}
|
|
387
397
|
/**
|
|
388
|
-
* Resolve a stable project/repo name from a working directory
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
* basename. Pure — no filesystem access, so it works for remote events too.
|
|
398
|
+
* Resolve a stable project/repo name from a working directory — the same fold
|
|
399
|
+
* the `agents sessions` overview groups by ({@link projectKeyFromCwd}), so a
|
|
400
|
+
* project reads identically in both views.
|
|
392
401
|
*/
|
|
393
402
|
export function projectFromCwd(cwd) {
|
|
394
|
-
|
|
395
|
-
return undefined;
|
|
396
|
-
const norm = cwd.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
397
|
-
if (!norm)
|
|
398
|
-
return undefined;
|
|
399
|
-
const wtIdx = norm.indexOf('/.agents/worktrees/');
|
|
400
|
-
if (wtIdx > 0) {
|
|
401
|
-
const repoPath = norm.slice(0, wtIdx);
|
|
402
|
-
const base = repoPath.slice(repoPath.lastIndexOf('/') + 1);
|
|
403
|
-
if (base)
|
|
404
|
-
return base;
|
|
405
|
-
}
|
|
406
|
-
const base = norm.slice(norm.lastIndexOf('/') + 1);
|
|
407
|
-
return base || undefined;
|
|
403
|
+
return projectKeyFromCwd(cwd);
|
|
408
404
|
}
|
|
409
405
|
/**
|
|
410
406
|
* Join session facts (ticket / project / execution host) onto each event by
|
|
411
|
-
* `sessionId`. A hint wins; otherwise
|
|
412
|
-
*
|
|
413
|
-
*
|
|
407
|
+
* `sessionId`. A hint wins; otherwise a CANONICAL def match (`canonicalProject`)
|
|
408
|
+
* upgrades whatever the event carries; otherwise pre-baked enriched fields (from
|
|
409
|
+
* a remote peer that already enriched its own stream) are preserved, and
|
|
410
|
+
* project/host fall back to what the event itself carries (`cwd`, `host`).
|
|
411
|
+
*
|
|
412
|
+
* The def match ranks above the pre-baked stamp because the stamp may be stale
|
|
413
|
+
* (posted before the def existed) or skewed (a peer whose defs haven't synced);
|
|
414
|
+
* the match is pure prefix compare against local defs, so a different-home
|
|
415
|
+
* peer's path simply doesn't match and its stamp is honored as before.
|
|
416
|
+
*
|
|
417
|
+
* `resolveProject` is how a caller reading its OWN machine's logs upgrades the
|
|
418
|
+
* cwd fold to real repository detection ({@link resolveProjectKey}); it defaults
|
|
419
|
+
* to the pure {@link projectFromCwd}, which is all a path from another machine
|
|
420
|
+
* can be resolved with. Pure given pure resolvers.
|
|
414
421
|
*/
|
|
415
|
-
export function enrichActivityEvents(events, hints) {
|
|
422
|
+
export function enrichActivityEvents(events, hints, resolveProject = projectFromCwd, canonicalProject) {
|
|
416
423
|
const bySession = new Map();
|
|
417
424
|
for (const h of hints)
|
|
418
425
|
if (h.sessionId)
|
|
419
426
|
bySession.set(h.sessionId, h);
|
|
420
427
|
return events.map((ev) => {
|
|
421
428
|
const hint = ev.sessionId ? bySession.get(ev.sessionId) : undefined;
|
|
422
|
-
const project = hint?.project ?? ev.project ??
|
|
429
|
+
const project = hint?.project ?? canonicalProject?.(ev.cwd) ?? ev.project ?? resolveProject(ev.cwd);
|
|
423
430
|
const ticket = hint?.ticket ?? ev.ticket ?? undefined;
|
|
424
431
|
const executionHost = hint?.executionHost ?? ev.executionHost
|
|
425
432
|
?? (ev.host && ev.host !== 'unknown' ? ev.host : undefined);
|
|
@@ -475,6 +482,41 @@ export function mergeActivityEvents(...groups) {
|
|
|
475
482
|
}
|
|
476
483
|
return [...byKey.values()].sort((a, b) => Date.parse(b.ts) - Date.parse(a.ts));
|
|
477
484
|
}
|
|
485
|
+
/**
|
|
486
|
+
* Apply `--limit` to the events a reader will actually SHOW.
|
|
487
|
+
*
|
|
488
|
+
* The default view collapses routine work (`file.edited`) to a count and shows
|
|
489
|
+
* milestones individually, so a plain `slice(limit)` spends the whole budget on
|
|
490
|
+
* churn: one busy machine editing 40 files hid every other device's PRs behind
|
|
491
|
+
* a single `file edited ×40` line. Milestones therefore carry the cap, and the
|
|
492
|
+
* routine events that ride along are the ones newer than the last milestone
|
|
493
|
+
* kept — so the collapsed counts describe exactly the window on screen.
|
|
494
|
+
*
|
|
495
|
+
* With `all` (routine shown inline) every event is displayed, so the cap is the
|
|
496
|
+
* plain slice again. Input must be newest-first; output preserves that order.
|
|
497
|
+
*/
|
|
498
|
+
export function capActivityEvents(events, limit, opts = {}) {
|
|
499
|
+
if (!Number.isFinite(limit) || limit <= 0)
|
|
500
|
+
return [];
|
|
501
|
+
if (opts.all)
|
|
502
|
+
return events.slice(0, limit);
|
|
503
|
+
const out = [];
|
|
504
|
+
let milestones = 0;
|
|
505
|
+
let lastMilestoneIdx = -1;
|
|
506
|
+
for (const ev of events) {
|
|
507
|
+
if (tierForEvent(ev.event) === 'milestone') {
|
|
508
|
+
if (milestones >= limit) {
|
|
509
|
+
// The cap is reached: the window ends at the oldest milestone kept, so
|
|
510
|
+
// trailing routine events (older than it) are outside it.
|
|
511
|
+
return out.slice(0, lastMilestoneIdx + 1);
|
|
512
|
+
}
|
|
513
|
+
milestones += 1;
|
|
514
|
+
lastMilestoneIdx = out.length;
|
|
515
|
+
}
|
|
516
|
+
out.push(ev);
|
|
517
|
+
}
|
|
518
|
+
return out;
|
|
519
|
+
}
|
|
478
520
|
/** The (key, label) an event sorts under for a given grouping dimension. */
|
|
479
521
|
export function activityGroupKey(ev, by) {
|
|
480
522
|
if (by === 'project') {
|
|
@@ -527,6 +569,18 @@ export function filterActivityEvents(events, filter) {
|
|
|
527
569
|
return fields.some((f) => typeof f === 'string' && f.toLowerCase().includes(needle));
|
|
528
570
|
});
|
|
529
571
|
}
|
|
572
|
+
/**
|
|
573
|
+
* Narrow events to one resolved project, exact match on the enriched label
|
|
574
|
+
* (unlike {@link filterActivityEvents}'s cross-field substring). This is the
|
|
575
|
+
* `--project` flag: the label is canonical (a defined project's name) once the
|
|
576
|
+
* reader's resolver has run, so a multi-repo project matches as one bucket. Pure.
|
|
577
|
+
*/
|
|
578
|
+
export function filterActivityByProject(events, project) {
|
|
579
|
+
const name = project.trim();
|
|
580
|
+
if (!name)
|
|
581
|
+
return events;
|
|
582
|
+
return events.filter((ev) => ev.project === name);
|
|
583
|
+
}
|
|
530
584
|
/** One rendered enriched line: the base activity line + `· project · ticket` tags. */
|
|
531
585
|
export function formatEnrichedActivityLine(ev, opts = {}) {
|
|
532
586
|
const base = formatActivityLine(ev, { showHost: opts.showHost });
|
|
@@ -538,15 +592,50 @@ export function formatEnrichedActivityLine(ev, opts = {}) {
|
|
|
538
592
|
const suffix = tags.length > 0 ? chalk.gray(` · ${tags.join(' · ')}`) : '';
|
|
539
593
|
return `${opts.indent ?? ''}${base}${suffix}`;
|
|
540
594
|
}
|
|
541
|
-
/**
|
|
542
|
-
export
|
|
595
|
+
/** Max device names named in a group header before the rest collapse to `+N`. */
|
|
596
|
+
export const GROUP_HEADER_DEVICE_LIMIT = 3;
|
|
597
|
+
/**
|
|
598
|
+
* The distinct machines a group's events ran on, most-active first then
|
|
599
|
+
* alphabetical — so a project header can say WHERE the work happened without
|
|
600
|
+
* sub-grouping the timeline. Prefers the session-joined `executionHost` (where
|
|
601
|
+
* the process really lives) over the raw emitting `host`. Pure.
|
|
602
|
+
*/
|
|
603
|
+
export function activityGroupDevices(events) {
|
|
604
|
+
const counts = new Map();
|
|
605
|
+
for (const ev of events) {
|
|
606
|
+
const host = ev.executionHost ?? (ev.host && ev.host !== 'unknown' ? ev.host : undefined);
|
|
607
|
+
if (!host)
|
|
608
|
+
continue;
|
|
609
|
+
counts.set(host, (counts.get(host) ?? 0) + 1);
|
|
610
|
+
}
|
|
611
|
+
return [...counts.entries()]
|
|
612
|
+
.sort((a, b) => (b[1] !== a[1] ? b[1] - a[1] : a[0].localeCompare(b[0])))
|
|
613
|
+
.map(([host]) => host);
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* The trailing facts for one grouped bucket: `N events · M milestones`, plus
|
|
617
|
+
* the machines the work ran on when `showDevices` is set (redundant when the
|
|
618
|
+
* grouping dimension IS the device, so the caller decides). The device list is
|
|
619
|
+
* capped at {@link GROUP_HEADER_DEVICE_LIMIT} with a `+N` tail, so a project
|
|
620
|
+
* touched by a dozen boxes stays one scannable line. Separate from the label so
|
|
621
|
+
* a renderer can color the two differently without re-splitting a string.
|
|
622
|
+
*/
|
|
623
|
+
export function formatActivityGroupMeta(group, opts = {}) {
|
|
543
624
|
const { milestones } = collapseActivity(group.events);
|
|
544
625
|
const n = group.events.length;
|
|
545
626
|
const m = milestones.length;
|
|
546
627
|
const parts = [`${n} event${n === 1 ? '' : 's'}`];
|
|
547
628
|
if (m > 0)
|
|
548
629
|
parts.push(`${m} milestone${m === 1 ? '' : 's'}`);
|
|
549
|
-
|
|
630
|
+
if (opts.showDevices) {
|
|
631
|
+
const devices = activityGroupDevices(group.events);
|
|
632
|
+
if (devices.length > 0) {
|
|
633
|
+
const named = devices.slice(0, GROUP_HEADER_DEVICE_LIMIT);
|
|
634
|
+
const rest = devices.length - named.length;
|
|
635
|
+
parts.push(rest > 0 ? `${named.join(', ')} +${rest}` : named.join(', '));
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return parts.join(' · ');
|
|
550
639
|
}
|
|
551
640
|
// ---------------------------------------------------------------------------
|
|
552
641
|
// Hook installation
|
|
@@ -561,15 +650,15 @@ export function formatActivityGroupHeader(group) {
|
|
|
561
650
|
* read-only tools never pay the hook cost. Fail-open: any error is swallowed so
|
|
562
651
|
* a logging hiccup never blocks a tool call.
|
|
563
652
|
*/
|
|
564
|
-
export const ACTIVITY_LOG_HOOK_SCRIPT = `#!/usr/bin/env python3
|
|
565
|
-
"""Append agent-activity events for
|
|
653
|
+
export const ACTIVITY_LOG_HOOK_SCRIPT = String.raw `#!/usr/bin/env python3
|
|
654
|
+
"""Append agent-activity events for 'agents feed' / 'agents activity'.
|
|
566
655
|
|
|
567
656
|
Bound to PreToolUse (ExitPlanMode, Task) and PostToolUse (Bash, Write, Edit,
|
|
568
657
|
MultiEdit, TodoWrite, update_plan, TaskUpdate, todo_write, TaskCreate). One
|
|
569
658
|
append-only file per session; read-only tools never trigger it because the
|
|
570
659
|
manifest matcher excludes them.
|
|
571
660
|
|
|
572
|
-
Sub-agent gate: when the payload carries
|
|
661
|
+
Sub-agent gate: when the payload carries 'agent_type', this is a Task/Agent
|
|
573
662
|
sub-agent -- skip so only the top-level agent logs its own activity.
|
|
574
663
|
|
|
575
664
|
Fail-open: ANY error is swallowed so a logging hiccup never blocks a tool call.
|
|
@@ -586,7 +675,9 @@ MAX_LOG_BYTES = 5 * 1024 * 1024 # cap a pathological session's log
|
|
|
586
675
|
MILESTONE_EVENTS = {
|
|
587
676
|
"plan.created", "pr.opened", "pr.merged", "worktree.created",
|
|
588
677
|
"worktree.removed", "commit.created", "pushed", "subagent.spawned",
|
|
589
|
-
"artifact.created", "task.completed", "checklist.created",
|
|
678
|
+
"artifact.created", "task.completed", "checklist.created",
|
|
679
|
+
"video.rendered", "video.converted", "image.upscaled", "metadata.edited",
|
|
680
|
+
"status.posted",
|
|
590
681
|
}
|
|
591
682
|
|
|
592
683
|
# Deliverable file types + locations -- a Write here is a recognizable artifact
|
|
@@ -621,54 +712,293 @@ def first_line(text, limit=140):
|
|
|
621
712
|
return ""
|
|
622
713
|
|
|
623
714
|
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
715
|
+
# Canonical command taxonomy for Bash tool calls. Mirrors the TypeScript
|
|
716
|
+
# registry in lib/session/bash-command.ts; keep them in sync.
|
|
717
|
+
BASH_TOOL_REGISTRY = {
|
|
718
|
+
"git": {"category": "vcs", "action": "working in git"},
|
|
719
|
+
"gh": {"category": "vcs", "action": "using GitHub CLI"},
|
|
720
|
+
"bun": {"category": "build-test", "action": "running bun"},
|
|
721
|
+
"npm": {"category": "build-test", "action": "running npm"},
|
|
722
|
+
"pnpm": {"category": "build-test", "action": "running pnpm"},
|
|
723
|
+
"yarn": {"category": "build-test", "action": "running yarn"},
|
|
724
|
+
"vitest": {"category": "build-test", "action": "running vitest"},
|
|
725
|
+
"jest": {"category": "build-test", "action": "running jest"},
|
|
726
|
+
"mocha": {"category": "build-test", "action": "running mocha"},
|
|
727
|
+
"pytest": {"category": "build-test", "action": "running pytest"},
|
|
728
|
+
"cargo": {"category": "build-test", "action": "running cargo"},
|
|
729
|
+
"go": {"category": "build-test", "action": "running go"},
|
|
730
|
+
"tsc": {"category": "build-test", "action": "running tsc"},
|
|
731
|
+
"tsx": {"category": "build-test", "action": "running tsx"},
|
|
732
|
+
"node": {"category": "build-test", "action": "running node"},
|
|
733
|
+
"python": {"category": "build-test", "action": "running python"},
|
|
734
|
+
"python3": {"category": "build-test", "action": "running python"},
|
|
735
|
+
"make": {"category": "build-test", "action": "running make"},
|
|
736
|
+
"brew": {"category": "install", "action": "installing with brew"},
|
|
737
|
+
"pip": {"category": "install", "action": "installing with pip"},
|
|
738
|
+
"pip3": {"category": "install", "action": "installing with pip"},
|
|
739
|
+
"apt": {"category": "install", "action": "installing with apt"},
|
|
740
|
+
"apk": {"category": "install", "action": "installing with apk"},
|
|
741
|
+
"ssh": {"category": "remote", "action": "using ssh"},
|
|
742
|
+
"scp": {"category": "remote", "action": "using scp"},
|
|
743
|
+
"rsync": {"category": "remote", "action": "using rsync"},
|
|
744
|
+
"curl": {"category": "http", "action": "fetching with curl"},
|
|
745
|
+
"wget": {"category": "http", "action": "fetching with wget"},
|
|
746
|
+
"ffmpeg": {"category": "media", "action": "using ffmpeg"},
|
|
747
|
+
"ffprobe": {"category": "media", "action": "probing media"},
|
|
748
|
+
"magick": {"category": "media", "action": "using ImageMagick"},
|
|
749
|
+
"convert": {"category": "media", "action": "converting images"},
|
|
750
|
+
"composite": {"category": "media", "action": "compositing images"},
|
|
751
|
+
"montage": {"category": "media", "action": "montaging images"},
|
|
752
|
+
"identify": {"category": "media", "action": "identifying images"},
|
|
753
|
+
"realesrgan": {"category": "upscaling", "action": "upscaling with realesrgan"},
|
|
754
|
+
"realesrgan-ncnn-vulkan": {"category": "upscaling", "action": "upscaling with realesrgan"},
|
|
755
|
+
"waifu2x": {"category": "upscaling", "action": "upscaling with waifu2x"},
|
|
756
|
+
"waifu2x-caffe": {"category": "upscaling", "action": "upscaling with waifu2x"},
|
|
757
|
+
"waifu2x-converter-cpp": {"category": "upscaling", "action": "upscaling with waifu2x"},
|
|
758
|
+
"swin2sr": {"category": "upscaling", "action": "upscaling with swin2sr"},
|
|
759
|
+
"resdet": {"category": "upscaling", "action": "detecting upscale"},
|
|
760
|
+
"id3v2": {"category": "metadata", "action": "editing id3 tags"},
|
|
761
|
+
"exiftool": {"category": "metadata", "action": "editing exif metadata"},
|
|
762
|
+
"metaflac": {"category": "metadata", "action": "editing flac metadata"},
|
|
763
|
+
"vorbiscomment": {"category": "metadata", "action": "editing vorbis comments"},
|
|
764
|
+
# Shell
|
|
765
|
+
"rm": {"category": "shell", "action": "removing files"},
|
|
766
|
+
"mv": {"category": "shell", "action": "moving files"},
|
|
767
|
+
"cp": {"category": "shell", "action": "copying files"},
|
|
768
|
+
"mkdir": {"category": "shell", "action": "making directories"},
|
|
769
|
+
"touch": {"category": "shell", "action": "touching files"},
|
|
770
|
+
"echo": {"category": "shell", "action": "echoing"},
|
|
771
|
+
"printf": {"category": "shell", "action": "printing"},
|
|
772
|
+
"chmod": {"category": "shell", "action": "changing permissions"},
|
|
773
|
+
"ln": {"category": "shell", "action": "linking files"},
|
|
774
|
+
"awk": {"category": "shell", "action": "running awk"},
|
|
775
|
+
"sed": {"category": "shell", "action": "running sed"},
|
|
776
|
+
"tee": {"category": "shell", "action": "teeing output"},
|
|
777
|
+
"xargs": {"category": "shell", "action": "running xargs"},
|
|
778
|
+
# Probes
|
|
779
|
+
"ls": {"category": "probe", "action": "listing files"},
|
|
780
|
+
"cat": {"category": "probe", "action": "reading files"},
|
|
781
|
+
"head": {"category": "probe", "action": "reading files"},
|
|
782
|
+
"tail": {"category": "probe", "action": "reading files"},
|
|
783
|
+
"wc": {"category": "probe", "action": "counting"},
|
|
784
|
+
"stat": {"category": "probe", "action": "statting files"},
|
|
785
|
+
"file": {"category": "probe", "action": "inspecting files"},
|
|
786
|
+
"which": {"category": "probe", "action": "locating binaries"},
|
|
787
|
+
"tree": {"category": "probe", "action": "listing files"},
|
|
788
|
+
"pwd": {"category": "probe", "action": "printing pwd"},
|
|
789
|
+
# Search
|
|
790
|
+
"grep": {"category": "search", "action": "searching"},
|
|
791
|
+
"rg": {"category": "search", "action": "searching with ripgrep"},
|
|
792
|
+
"ag": {"category": "search", "action": "searching with the silver searcher"},
|
|
793
|
+
"fd": {"category": "search", "action": "searching files"},
|
|
794
|
+
"find": {"category": "search", "action": "finding files"},
|
|
795
|
+
# Wait
|
|
796
|
+
"sleep": {"category": "wait", "action": "sleeping"},
|
|
797
|
+
"wait": {"category": "wait", "action": "waiting"},
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
# Two-level tools mapped to the flags that consume the following token as their
|
|
801
|
+
# value, per tool; the subcommand scan skips both. Missing a value flag mis-reads
|
|
802
|
+
# the value as the subcommand; over-listing only drops the subcommand (safe). A
|
|
803
|
+
# tool with no such leading flags maps to an empty set. Mirrors VALUE_FLAGS in
|
|
804
|
+
# lib/session/bash-command.ts; TWO_LEVEL_TOOLS is derived from its keys.
|
|
805
|
+
VALUE_FLAGS = {
|
|
806
|
+
"git": {"-C", "-c", "--git-dir", "--work-tree"},
|
|
807
|
+
"gh": {"-R", "--repo"},
|
|
808
|
+
"bun": {"--cwd"},
|
|
809
|
+
"npm": {"--prefix", "-w", "--workspace"},
|
|
810
|
+
"pnpm": {"--filter", "-C", "--dir"},
|
|
811
|
+
"yarn": {"--cwd"},
|
|
812
|
+
"cargo": {"--manifest-path"},
|
|
813
|
+
"docker": {"-H", "--host", "-c", "--context", "--config", "-l", "--log-level"},
|
|
814
|
+
"kubectl": {"-n", "--namespace", "--kubeconfig", "--context", "--cluster", "--user", "-s", "--server", "--as", "--token", "--cache-dir", "--request-timeout"},
|
|
815
|
+
"rush": set(),
|
|
816
|
+
"openclaw": set(),
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
TWO_LEVEL_TOOLS = set(VALUE_FLAGS.keys())
|
|
820
|
+
|
|
821
|
+
|
|
822
|
+
def _unwrap_command(cmd):
|
|
823
|
+
"""Strip wrapper prefixes so the real executable is classified."""
|
|
824
|
+
s = (cmd or "").strip()
|
|
825
|
+
ssh = re.match(r'^ssh\s+\S+\s+["\']?(.+?)["\']?\s*(?:\|.*)?$', s)
|
|
826
|
+
if ssh:
|
|
827
|
+
return _unwrap_command(ssh.group(1))
|
|
828
|
+
# VAR=value prefix (value may be a single- or double-quoted string with spaces)
|
|
829
|
+
env = re.match(r'^([A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|' + r"'[^']*'" + r'|\S+)\s+)+(.+)$', s)
|
|
830
|
+
if env:
|
|
831
|
+
return _unwrap_command(env.group(2))
|
|
832
|
+
# sudo / time prefix (value-taking flags such as -u user consume their argument)
|
|
833
|
+
prefix = re.match(r'^(?:sudo|time)(?:\s+(?:-[uUgGhpCrtDR]\s+\S+|-\S+))*\s+(.+)$', s)
|
|
834
|
+
if prefix:
|
|
835
|
+
return _unwrap_command(prefix.group(1))
|
|
836
|
+
cd = re.match(r'^cd\s+\S+\s*&&\s*(.+)$', s)
|
|
837
|
+
if cd:
|
|
838
|
+
return _unwrap_command(cd.group(1))
|
|
839
|
+
npx = re.match(r'^(?:npx|bunx)\s+(?:-\S+\s+)*(.+)$', s)
|
|
840
|
+
if npx:
|
|
841
|
+
return _unwrap_command(npx.group(1))
|
|
842
|
+
return s
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
def _split_on_operators(cmd):
|
|
846
|
+
"""Split a command on && || | ; while respecting quotes and escapes."""
|
|
847
|
+
parts = []
|
|
848
|
+
current = ""
|
|
849
|
+
quote = None
|
|
850
|
+
escaped = False
|
|
851
|
+
i = 0
|
|
852
|
+
while i < len(cmd):
|
|
853
|
+
ch = cmd[i]
|
|
854
|
+
if escaped:
|
|
855
|
+
current += ch
|
|
856
|
+
escaped = False
|
|
857
|
+
i += 1
|
|
858
|
+
continue
|
|
859
|
+
if ch == "\\":
|
|
860
|
+
current += ch
|
|
861
|
+
escaped = True
|
|
862
|
+
i += 1
|
|
863
|
+
continue
|
|
864
|
+
if quote:
|
|
865
|
+
current += ch
|
|
866
|
+
if ch == quote:
|
|
867
|
+
quote = None
|
|
868
|
+
i += 1
|
|
869
|
+
continue
|
|
870
|
+
if ch in ('"', "'"):
|
|
871
|
+
current += ch
|
|
872
|
+
quote = ch
|
|
873
|
+
i += 1
|
|
874
|
+
continue
|
|
875
|
+
if cmd[i:i+2] in ("&&", "||"):
|
|
876
|
+
if current.strip():
|
|
877
|
+
parts.append(current.strip())
|
|
878
|
+
current = ""
|
|
879
|
+
i += 2
|
|
880
|
+
continue
|
|
881
|
+
if ch in ("|", ";"):
|
|
882
|
+
if current.strip():
|
|
883
|
+
parts.append(current.strip())
|
|
884
|
+
current = ""
|
|
885
|
+
i += 1
|
|
886
|
+
continue
|
|
887
|
+
current += ch
|
|
888
|
+
i += 1
|
|
889
|
+
if current.strip():
|
|
890
|
+
parts.append(current.strip())
|
|
891
|
+
return parts
|
|
892
|
+
|
|
893
|
+
|
|
894
|
+
def _tokenize_bash(command):
|
|
895
|
+
"""Return a list of token lists, one per simple command."""
|
|
896
|
+
unwrapped = _unwrap_command(command)
|
|
897
|
+
segments = _split_on_operators(unwrapped)
|
|
898
|
+
out = []
|
|
899
|
+
for seg in segments:
|
|
900
|
+
try:
|
|
901
|
+
tokens = shlex.split(seg)
|
|
902
|
+
except Exception:
|
|
903
|
+
tokens = seg.split()
|
|
904
|
+
if tokens:
|
|
905
|
+
out.append(tokens)
|
|
906
|
+
return out
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
def _scan_subcommand(tokens, tool):
|
|
910
|
+
"""First non-flag token after the executable, skipping flags and the argument
|
|
911
|
+
of a value-taking flag for that tool. Mirrors scanSubcommand in bash-command.ts."""
|
|
912
|
+
value_flags = VALUE_FLAGS.get(tool, set())
|
|
913
|
+
i = 1
|
|
631
914
|
while i < len(tokens):
|
|
632
915
|
t = tokens[i]
|
|
633
|
-
if t in ("-C", "-c", "--git-dir", "--work-tree"):
|
|
634
|
-
i += 2 # flag that consumes the next token
|
|
635
|
-
continue
|
|
636
916
|
if t.startswith("-"):
|
|
637
|
-
i += 1
|
|
917
|
+
i += 2 if t in value_flags else 1
|
|
638
918
|
continue
|
|
639
|
-
return t
|
|
640
|
-
return
|
|
919
|
+
return t.lower()
|
|
920
|
+
return ""
|
|
641
921
|
|
|
642
922
|
|
|
643
|
-
def
|
|
644
|
-
"""Return
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
if
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
923
|
+
def classify_bash_command(command):
|
|
924
|
+
"""Return {tool, category, subcommand, action, summary} for the first simple command."""
|
|
925
|
+
simple_commands = _tokenize_bash(command)
|
|
926
|
+
if not simple_commands:
|
|
927
|
+
return {"tool": "other", "category": "other", "subcommand": "", "action": "running command", "summary": ""}
|
|
928
|
+
tokens = simple_commands[0]
|
|
929
|
+
if not tokens:
|
|
930
|
+
return {"tool": "other", "category": "other", "subcommand": "", "action": "running command", "summary": ""}
|
|
931
|
+
|
|
932
|
+
first = tokens[0]
|
|
933
|
+
base = re.sub(r'^[./]+', '', first).lower()
|
|
934
|
+
if base.endswith(".exe"):
|
|
935
|
+
base = base[:-4]
|
|
936
|
+
info = BASH_TOOL_REGISTRY.get(base)
|
|
937
|
+
if not info:
|
|
938
|
+
# Known two-level tool absent from the registry (docker/kubectl/rush/
|
|
939
|
+
# openclaw) still surfaces its subcommand; category stays 'other'.
|
|
940
|
+
sub = _scan_subcommand(tokens, base) if base in TWO_LEVEL_TOOLS else ""
|
|
941
|
+
summary = "{} {}".format(base, sub) if sub else first
|
|
942
|
+
return {"tool": first, "category": "other", "subcommand": sub, "action": "running command", "summary": summary}
|
|
943
|
+
|
|
944
|
+
subcommand = _scan_subcommand(tokens, base) if base in TWO_LEVEL_TOOLS else ""
|
|
945
|
+
|
|
946
|
+
summary = "{} {}".format(base, subcommand) if subcommand else base
|
|
947
|
+
return {
|
|
948
|
+
"tool": base,
|
|
949
|
+
"category": info["category"],
|
|
950
|
+
"subcommand": subcommand,
|
|
951
|
+
"action": info["action"],
|
|
952
|
+
"summary": summary,
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
def detect_bash_milestone(command):
|
|
957
|
+
"""Return (event, detail) for high-signal Bash commands, else None."""
|
|
958
|
+
info = classify_bash_command(command)
|
|
959
|
+
lower = (command or "").lower()
|
|
960
|
+
|
|
961
|
+
if info["category"] == "upscaling":
|
|
962
|
+
return "image.upscaled", info["action"]
|
|
963
|
+
|
|
964
|
+
if info["tool"] == "ffmpeg":
|
|
965
|
+
has_output = re.search(r'\s+\S+\.\w{2,5}\s*$', command or "")
|
|
966
|
+
if has_output or "-c:v" in lower or "-codec" in lower or "libx264" in lower:
|
|
967
|
+
return "video.rendered", "ffmpeg render"
|
|
968
|
+
return "video.converted", "ffmpeg"
|
|
969
|
+
|
|
970
|
+
if info["category"] == "metadata":
|
|
971
|
+
return "metadata.edited", info["action"]
|
|
972
|
+
|
|
973
|
+
if info["tool"] == "git":
|
|
974
|
+
if info["subcommand"] == "commit":
|
|
975
|
+
return "commit.created", "git commit"
|
|
976
|
+
if info["subcommand"] == "push":
|
|
977
|
+
return "pushed", "git push"
|
|
978
|
+
if info["subcommand"] == "worktree":
|
|
979
|
+
if "worktree add" in lower:
|
|
980
|
+
return "worktree.created", "git worktree add"
|
|
981
|
+
if "worktree remove" in lower:
|
|
982
|
+
return "worktree.removed", "git worktree remove"
|
|
983
|
+
|
|
984
|
+
if info["tool"] == "gh" and info["subcommand"] == "pr":
|
|
985
|
+
if "pr create" in lower:
|
|
986
|
+
return "pr.opened", "gh pr create"
|
|
987
|
+
if "pr merge" in lower:
|
|
988
|
+
return "pr.merged", "gh pr merge"
|
|
989
|
+
|
|
669
990
|
return None
|
|
670
991
|
|
|
671
992
|
|
|
993
|
+
def first_line_of_command(command):
|
|
994
|
+
"""First non-empty line of a command, trimmed."""
|
|
995
|
+
for raw in (command or "").splitlines():
|
|
996
|
+
s = raw.strip()
|
|
997
|
+
if s:
|
|
998
|
+
return s[:140]
|
|
999
|
+
return ""
|
|
1000
|
+
|
|
1001
|
+
|
|
672
1002
|
def extract_url(tool_response):
|
|
673
1003
|
"""Pull the first https URL out of a Bash tool response (stdout)."""
|
|
674
1004
|
text = ""
|
|
@@ -676,7 +1006,7 @@ def extract_url(tool_response):
|
|
|
676
1006
|
text = str(tool_response.get("stdout") or tool_response.get("output") or "")
|
|
677
1007
|
elif isinstance(tool_response, str):
|
|
678
1008
|
text = tool_response
|
|
679
|
-
m = re.search(r"https
|
|
1009
|
+
m = re.search(r"https?://\S+", text)
|
|
680
1010
|
return m.group(0).rstrip(").,") if m else None
|
|
681
1011
|
|
|
682
1012
|
|
|
@@ -1107,10 +1437,24 @@ def build_event(payload, hook_event):
|
|
|
1107
1437
|
detail = (role + ": " + first_line(desc)).strip(": ").strip()
|
|
1108
1438
|
elif hook_event == "PostToolUse":
|
|
1109
1439
|
if tool_name == "Bash":
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1440
|
+
cmd = tool_input.get("command", "")
|
|
1441
|
+
info = classify_bash_command(cmd)
|
|
1442
|
+
records = []
|
|
1443
|
+
# Always emit a structured bash.executed activity event.
|
|
1444
|
+
bash_record = _make_record("bash.executed", info.get("summary") or first_line_of_command(cmd), tool_name)
|
|
1445
|
+
bash_record["category"] = info.get("category")
|
|
1446
|
+
bash_record["bashTool"] = info.get("tool")
|
|
1447
|
+
bash_record["bashAction"] = info.get("action")
|
|
1448
|
+
records.append(bash_record)
|
|
1449
|
+
milestone = detect_bash_milestone(cmd)
|
|
1450
|
+
if milestone:
|
|
1451
|
+
event, detail = milestone
|
|
1452
|
+
milestone_record = _make_record(event, detail, tool_name)
|
|
1113
1453
|
url = extract_url(tool_response)
|
|
1454
|
+
if url:
|
|
1455
|
+
milestone_record["url"] = url
|
|
1456
|
+
records.append(milestone_record)
|
|
1457
|
+
return records, tool_name
|
|
1114
1458
|
elif tool_name in ("Write", "Edit", "MultiEdit"):
|
|
1115
1459
|
fp = tool_input.get("file_path") or tool_input.get("path") or ""
|
|
1116
1460
|
# A freshly-written deliverable is a milestone; edits and code
|
|
@@ -1184,7 +1528,7 @@ def main():
|
|
|
1184
1528
|
for record in records:
|
|
1185
1529
|
if over_limit and record.get("tier") != "milestone":
|
|
1186
1530
|
continue
|
|
1187
|
-
f.write(json.dumps(record) + "
|
|
1531
|
+
f.write(json.dumps(record) + "\n")
|
|
1188
1532
|
except Exception:
|
|
1189
1533
|
pass # fail open
|
|
1190
1534
|
|
package/dist/lib/beta.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* checks.
|
|
9
9
|
*/
|
|
10
10
|
import type { BetaFeatureName } from './types.js';
|
|
11
|
-
export declare const ALL_BETA_FEATURES: readonly ["drive", "factory", "session-sync"];
|
|
11
|
+
export declare const ALL_BETA_FEATURES: readonly ["drive", "factory", "session-sync", "projects"];
|
|
12
12
|
export declare function getEnabledBetaFeatures(): BetaFeatureName[];
|
|
13
13
|
export declare function isBetaEnabled(feature: BetaFeatureName): boolean;
|
|
14
14
|
export declare function getBetaConfigLocation(): {
|
package/dist/lib/beta.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import * as path from 'path';
|
|
11
11
|
import { getAgentsDir, getOptionalUserAgentsDir, readMeta, writeMeta } from './state.js';
|
|
12
12
|
import { readManifest, writeManifest } from './manifest.js';
|
|
13
|
-
export const ALL_BETA_FEATURES = ['drive', 'factory', 'session-sync'];
|
|
13
|
+
export const ALL_BETA_FEATURES = ['drive', 'factory', 'session-sync', 'projects'];
|
|
14
14
|
function isBetaFeatureName(value) {
|
|
15
15
|
return typeof value === 'string' && ALL_BETA_FEATURES.includes(value);
|
|
16
16
|
}
|