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