@phnx-labs/agents-cli 1.20.91 → 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 +155 -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/projects.d.ts +12 -0
- package/dist/commands/projects.js +358 -0
- package/dist/commands/sessions-picker.d.ts +15 -0
- package/dist/commands/sessions-picker.js +37 -12
- 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 +2 -1
- package/dist/lib/activity.d.ts +69 -12
- package/dist/lib/activity.js +417 -74
- package/dist/lib/beta.d.ts +1 -1
- package/dist/lib/beta.js +1 -1
- package/dist/lib/devices/registry.d.ts +14 -0
- package/dist/lib/devices/registry.js +37 -0
- package/dist/lib/feed-post.js +8 -2
- 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/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/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -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 +6 -0
- package/dist/lib/session/digest.js +19 -0
- package/dist/lib/session/relative-time.d.ts +23 -0
- package/dist/lib/session/relative-time.js +60 -8
- package/dist/lib/session/remote-list.js +5 -2
- package/dist/lib/session/render.d.ts +2 -9
- package/dist/lib/session/render.js +25 -56
- 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 +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/state.d.ts +2 -0
- package/dist/lib/state.js +5 -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 +2 -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,6 +38,10 @@ 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',
|
|
41
46
|
'status.blocked',
|
|
42
47
|
];
|
|
@@ -131,6 +136,9 @@ function parseLine(line) {
|
|
|
131
136
|
launchId: parsed.launchId,
|
|
132
137
|
terminalId: parsed.terminalId,
|
|
133
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,
|
|
134
142
|
attachments: sanitizeAttachments(parsed.attachments),
|
|
135
143
|
};
|
|
136
144
|
}
|
|
@@ -294,6 +302,7 @@ export const EVENT_STYLE = {
|
|
|
294
302
|
'checklist.created': { glyph: '☐', color: chalk.cyan, label: 'checklist created' },
|
|
295
303
|
'status.posted': { glyph: '▸', color: chalk.white, label: 'status' },
|
|
296
304
|
'file.edited': { glyph: '·', color: chalk.gray, label: 'file edited' },
|
|
305
|
+
'bash.executed': { glyph: '$', color: chalk.gray, label: 'command run' },
|
|
297
306
|
};
|
|
298
307
|
export function styleForEvent(event) {
|
|
299
308
|
return EVENT_STYLE[event] ?? { glyph: '•', color: chalk.white, label: event };
|
|
@@ -386,41 +395,38 @@ export function formatProgressUpdate(ev, opts = {}) {
|
|
|
386
395
|
return lines.join('\n');
|
|
387
396
|
}
|
|
388
397
|
/**
|
|
389
|
-
* Resolve a stable project/repo name from a working directory
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
* 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.
|
|
393
401
|
*/
|
|
394
402
|
export function projectFromCwd(cwd) {
|
|
395
|
-
|
|
396
|
-
return undefined;
|
|
397
|
-
const norm = cwd.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
398
|
-
if (!norm)
|
|
399
|
-
return undefined;
|
|
400
|
-
const wtIdx = norm.indexOf('/.agents/worktrees/');
|
|
401
|
-
if (wtIdx > 0) {
|
|
402
|
-
const repoPath = norm.slice(0, wtIdx);
|
|
403
|
-
const base = repoPath.slice(repoPath.lastIndexOf('/') + 1);
|
|
404
|
-
if (base)
|
|
405
|
-
return base;
|
|
406
|
-
}
|
|
407
|
-
const base = norm.slice(norm.lastIndexOf('/') + 1);
|
|
408
|
-
return base || undefined;
|
|
403
|
+
return projectKeyFromCwd(cwd);
|
|
409
404
|
}
|
|
410
405
|
/**
|
|
411
406
|
* Join session facts (ticket / project / execution host) onto each event by
|
|
412
|
-
* `sessionId`. A hint wins; otherwise
|
|
413
|
-
*
|
|
414
|
-
*
|
|
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.
|
|
415
421
|
*/
|
|
416
|
-
export function enrichActivityEvents(events, hints) {
|
|
422
|
+
export function enrichActivityEvents(events, hints, resolveProject = projectFromCwd, canonicalProject) {
|
|
417
423
|
const bySession = new Map();
|
|
418
424
|
for (const h of hints)
|
|
419
425
|
if (h.sessionId)
|
|
420
426
|
bySession.set(h.sessionId, h);
|
|
421
427
|
return events.map((ev) => {
|
|
422
428
|
const hint = ev.sessionId ? bySession.get(ev.sessionId) : undefined;
|
|
423
|
-
const project = hint?.project ?? ev.project ??
|
|
429
|
+
const project = hint?.project ?? canonicalProject?.(ev.cwd) ?? ev.project ?? resolveProject(ev.cwd);
|
|
424
430
|
const ticket = hint?.ticket ?? ev.ticket ?? undefined;
|
|
425
431
|
const executionHost = hint?.executionHost ?? ev.executionHost
|
|
426
432
|
?? (ev.host && ev.host !== 'unknown' ? ev.host : undefined);
|
|
@@ -476,6 +482,41 @@ export function mergeActivityEvents(...groups) {
|
|
|
476
482
|
}
|
|
477
483
|
return [...byKey.values()].sort((a, b) => Date.parse(b.ts) - Date.parse(a.ts));
|
|
478
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
|
+
}
|
|
479
520
|
/** The (key, label) an event sorts under for a given grouping dimension. */
|
|
480
521
|
export function activityGroupKey(ev, by) {
|
|
481
522
|
if (by === 'project') {
|
|
@@ -528,6 +569,18 @@ export function filterActivityEvents(events, filter) {
|
|
|
528
569
|
return fields.some((f) => typeof f === 'string' && f.toLowerCase().includes(needle));
|
|
529
570
|
});
|
|
530
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
|
+
}
|
|
531
584
|
/** One rendered enriched line: the base activity line + `· project · ticket` tags. */
|
|
532
585
|
export function formatEnrichedActivityLine(ev, opts = {}) {
|
|
533
586
|
const base = formatActivityLine(ev, { showHost: opts.showHost });
|
|
@@ -539,15 +592,50 @@ export function formatEnrichedActivityLine(ev, opts = {}) {
|
|
|
539
592
|
const suffix = tags.length > 0 ? chalk.gray(` · ${tags.join(' · ')}`) : '';
|
|
540
593
|
return `${opts.indent ?? ''}${base}${suffix}`;
|
|
541
594
|
}
|
|
542
|
-
/**
|
|
543
|
-
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 = {}) {
|
|
544
624
|
const { milestones } = collapseActivity(group.events);
|
|
545
625
|
const n = group.events.length;
|
|
546
626
|
const m = milestones.length;
|
|
547
627
|
const parts = [`${n} event${n === 1 ? '' : 's'}`];
|
|
548
628
|
if (m > 0)
|
|
549
629
|
parts.push(`${m} milestone${m === 1 ? '' : 's'}`);
|
|
550
|
-
|
|
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(' · ');
|
|
551
639
|
}
|
|
552
640
|
// ---------------------------------------------------------------------------
|
|
553
641
|
// Hook installation
|
|
@@ -562,15 +650,15 @@ export function formatActivityGroupHeader(group) {
|
|
|
562
650
|
* read-only tools never pay the hook cost. Fail-open: any error is swallowed so
|
|
563
651
|
* a logging hiccup never blocks a tool call.
|
|
564
652
|
*/
|
|
565
|
-
export const ACTIVITY_LOG_HOOK_SCRIPT = `#!/usr/bin/env python3
|
|
566
|
-
"""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'.
|
|
567
655
|
|
|
568
656
|
Bound to PreToolUse (ExitPlanMode, Task) and PostToolUse (Bash, Write, Edit,
|
|
569
657
|
MultiEdit, TodoWrite, update_plan, TaskUpdate, todo_write, TaskCreate). One
|
|
570
658
|
append-only file per session; read-only tools never trigger it because the
|
|
571
659
|
manifest matcher excludes them.
|
|
572
660
|
|
|
573
|
-
Sub-agent gate: when the payload carries
|
|
661
|
+
Sub-agent gate: when the payload carries 'agent_type', this is a Task/Agent
|
|
574
662
|
sub-agent -- skip so only the top-level agent logs its own activity.
|
|
575
663
|
|
|
576
664
|
Fail-open: ANY error is swallowed so a logging hiccup never blocks a tool call.
|
|
@@ -587,7 +675,9 @@ MAX_LOG_BYTES = 5 * 1024 * 1024 # cap a pathological session's log
|
|
|
587
675
|
MILESTONE_EVENTS = {
|
|
588
676
|
"plan.created", "pr.opened", "pr.merged", "worktree.created",
|
|
589
677
|
"worktree.removed", "commit.created", "pushed", "subagent.spawned",
|
|
590
|
-
"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",
|
|
591
681
|
}
|
|
592
682
|
|
|
593
683
|
# Deliverable file types + locations -- a Write here is a recognizable artifact
|
|
@@ -622,54 +712,293 @@ def first_line(text, limit=140):
|
|
|
622
712
|
return ""
|
|
623
713
|
|
|
624
714
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
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
|
|
632
914
|
while i < len(tokens):
|
|
633
915
|
t = tokens[i]
|
|
634
|
-
if t in ("-C", "-c", "--git-dir", "--work-tree"):
|
|
635
|
-
i += 2 # flag that consumes the next token
|
|
636
|
-
continue
|
|
637
916
|
if t.startswith("-"):
|
|
638
|
-
i += 1
|
|
917
|
+
i += 2 if t in value_flags else 1
|
|
639
918
|
continue
|
|
640
|
-
return t
|
|
641
|
-
return
|
|
919
|
+
return t.lower()
|
|
920
|
+
return ""
|
|
642
921
|
|
|
643
922
|
|
|
644
|
-
def
|
|
645
|
-
"""Return
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
if
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
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
|
+
|
|
670
990
|
return None
|
|
671
991
|
|
|
672
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
|
+
|
|
673
1002
|
def extract_url(tool_response):
|
|
674
1003
|
"""Pull the first https URL out of a Bash tool response (stdout)."""
|
|
675
1004
|
text = ""
|
|
@@ -677,7 +1006,7 @@ def extract_url(tool_response):
|
|
|
677
1006
|
text = str(tool_response.get("stdout") or tool_response.get("output") or "")
|
|
678
1007
|
elif isinstance(tool_response, str):
|
|
679
1008
|
text = tool_response
|
|
680
|
-
m = re.search(r"https
|
|
1009
|
+
m = re.search(r"https?://\S+", text)
|
|
681
1010
|
return m.group(0).rstrip(").,") if m else None
|
|
682
1011
|
|
|
683
1012
|
|
|
@@ -1108,10 +1437,24 @@ def build_event(payload, hook_event):
|
|
|
1108
1437
|
detail = (role + ": " + first_line(desc)).strip(": ").strip()
|
|
1109
1438
|
elif hook_event == "PostToolUse":
|
|
1110
1439
|
if tool_name == "Bash":
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
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)
|
|
1114
1453
|
url = extract_url(tool_response)
|
|
1454
|
+
if url:
|
|
1455
|
+
milestone_record["url"] = url
|
|
1456
|
+
records.append(milestone_record)
|
|
1457
|
+
return records, tool_name
|
|
1115
1458
|
elif tool_name in ("Write", "Edit", "MultiEdit"):
|
|
1116
1459
|
fp = tool_input.get("file_path") or tool_input.get("path") or ""
|
|
1117
1460
|
# A freshly-written deliverable is a milestone; edits and code
|
|
@@ -1185,7 +1528,7 @@ def main():
|
|
|
1185
1528
|
for record in records:
|
|
1186
1529
|
if over_limit and record.get("tier") != "milestone":
|
|
1187
1530
|
continue
|
|
1188
|
-
f.write(json.dumps(record) + "
|
|
1531
|
+
f.write(json.dumps(record) + "\n")
|
|
1189
1532
|
except Exception:
|
|
1190
1533
|
pass # fail open
|
|
1191
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
|
}
|