@phnx-labs/agents-cli 1.19.2 → 1.20.3
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 +140 -0
- package/README.md +72 -12
- package/dist/browser.js +0 -0
- package/dist/commands/browser.js +88 -16
- package/dist/commands/cli.d.ts +14 -0
- package/dist/commands/cli.js +244 -0
- package/dist/commands/cloud.js +1 -1
- package/dist/commands/commands.js +27 -10
- package/dist/commands/computer.js +18 -1
- package/dist/commands/doctor.d.ts +1 -1
- package/dist/commands/doctor.js +2 -2
- package/dist/commands/exec.js +38 -18
- package/dist/commands/factory.d.ts +3 -14
- package/dist/commands/factory.js +3 -3
- package/dist/commands/feedback.d.ts +7 -0
- package/dist/commands/feedback.js +89 -0
- package/dist/commands/helper.d.ts +12 -0
- package/dist/commands/helper.js +87 -0
- package/dist/commands/hooks.js +89 -10
- package/dist/commands/mcp.js +166 -10
- package/dist/commands/packages.js +196 -27
- package/dist/commands/permissions.js +21 -6
- package/dist/commands/plugins.js +11 -4
- package/dist/commands/profiles.d.ts +8 -0
- package/dist/commands/profiles.js +118 -5
- package/dist/commands/prune.js +39 -160
- package/dist/commands/pull.js +58 -5
- package/dist/commands/routines.js +107 -14
- package/dist/commands/rules.js +8 -4
- package/dist/commands/secrets-migrate.d.ts +24 -0
- package/dist/commands/secrets-migrate.js +198 -0
- package/dist/commands/secrets-sync.d.ts +11 -0
- package/dist/commands/secrets-sync.js +155 -0
- package/dist/commands/secrets.js +79 -46
- package/dist/commands/sessions.d.ts +28 -0
- package/dist/commands/sessions.js +98 -33
- package/dist/commands/setup.d.ts +1 -0
- package/dist/commands/setup.js +37 -28
- package/dist/commands/skills.js +25 -8
- package/dist/commands/subagents.js +69 -49
- package/dist/commands/teams.js +61 -10
- package/dist/commands/utils.d.ts +33 -0
- package/dist/commands/utils.js +139 -0
- package/dist/commands/versions.d.ts +4 -3
- package/dist/commands/versions.js +134 -130
- package/dist/commands/view.d.ts +6 -0
- package/dist/commands/view.js +175 -19
- package/dist/commands/workflows.js +29 -6
- package/dist/computer.js +0 -0
- package/dist/index.js +38 -6
- package/dist/lib/acp/client.js +6 -1
- package/dist/lib/acp/harnesses.js +8 -0
- package/dist/lib/agents.d.ts +4 -0
- package/dist/lib/agents.js +125 -34
- package/dist/lib/auto-pull-worker.js +18 -1
- package/dist/lib/browser/cdp.d.ts +8 -1
- package/dist/lib/browser/cdp.js +40 -3
- package/dist/lib/browser/chrome.d.ts +13 -0
- package/dist/lib/browser/chrome.js +46 -3
- package/dist/lib/browser/domain-skills.d.ts +51 -0
- package/dist/lib/browser/domain-skills.js +157 -0
- package/dist/lib/browser/drivers/local.js +45 -4
- package/dist/lib/browser/drivers/ssh.js +2 -2
- package/dist/lib/browser/ipc.d.ts +8 -1
- package/dist/lib/browser/ipc.js +37 -28
- package/dist/lib/browser/profiles.d.ts +16 -3
- package/dist/lib/browser/profiles.js +44 -4
- package/dist/lib/browser/service.d.ts +3 -0
- package/dist/lib/browser/service.js +40 -5
- package/dist/lib/browser/types.d.ts +11 -4
- package/dist/lib/cli-resources.d.ts +137 -0
- package/dist/lib/cli-resources.js +477 -0
- package/dist/lib/cloud/factory.d.ts +1 -1
- package/dist/lib/cloud/factory.js +1 -1
- package/dist/lib/cloud/rush.js +5 -5
- package/dist/lib/command-skills.js +0 -2
- package/dist/lib/computer-rpc.d.ts +3 -0
- package/dist/lib/computer-rpc.js +53 -0
- package/dist/lib/daemon.js +20 -0
- package/dist/lib/events.d.ts +16 -2
- package/dist/lib/events.js +33 -2
- package/dist/lib/exec.d.ts +42 -13
- package/dist/lib/exec.js +127 -33
- package/dist/lib/help.js +11 -5
- package/dist/lib/hooks/cache.d.ts +38 -0
- package/dist/lib/hooks/cache.js +242 -0
- package/dist/lib/hooks/profile.d.ts +33 -0
- package/dist/lib/hooks/profile.js +129 -0
- package/dist/lib/hooks.d.ts +0 -10
- package/dist/lib/hooks.js +246 -11
- package/dist/lib/mcp.d.ts +15 -0
- package/dist/lib/mcp.js +46 -0
- package/dist/lib/migrate.js +1 -1
- package/dist/lib/overdue.d.ts +26 -0
- package/dist/lib/overdue.js +101 -0
- package/dist/lib/permissions.d.ts +13 -0
- package/dist/lib/permissions.js +55 -1
- package/dist/lib/plugin-marketplace.js +1 -1
- package/dist/lib/plugins.js +15 -1
- package/dist/lib/profiles-presets.d.ts +26 -0
- package/dist/lib/profiles-presets.js +216 -0
- package/dist/lib/profiles.d.ts +34 -0
- package/dist/lib/profiles.js +112 -1
- package/dist/lib/resources/mcp.js +37 -0
- package/dist/lib/resources.d.ts +1 -1
- package/dist/lib/rotate.js +10 -4
- package/dist/lib/routines-format.d.ts +47 -0
- package/dist/lib/routines-format.js +194 -0
- package/dist/lib/routines.d.ts +8 -2
- package/dist/lib/routines.js +34 -14
- package/dist/lib/runner.js +83 -15
- package/dist/lib/scheduler.js +8 -1
- 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/Agents CLI.app/Contents/_CodeSignature/CodeResources +1 -9
- package/dist/lib/secrets/bundles.d.ts +34 -17
- package/dist/lib/secrets/bundles.js +210 -36
- package/dist/lib/secrets/index.d.ts +49 -30
- package/dist/lib/secrets/index.js +126 -115
- package/dist/lib/secrets/install-helper.d.ts +45 -0
- package/dist/lib/secrets/install-helper.js +165 -0
- package/dist/lib/secrets/linux.js +4 -4
- package/dist/lib/secrets/sync.d.ts +56 -0
- package/dist/lib/secrets/sync.js +180 -0
- package/dist/lib/session/active.d.ts +8 -0
- package/dist/lib/session/active.js +3 -2
- package/dist/lib/session/db.d.ts +0 -4
- package/dist/lib/session/db.js +0 -26
- package/dist/lib/session/parse.d.ts +1 -0
- package/dist/lib/session/parse.js +44 -0
- package/dist/lib/session/render.js +4 -4
- package/dist/lib/session/types.d.ts +2 -2
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/shims.d.ts +5 -2
- package/dist/lib/shims.js +70 -38
- package/dist/lib/state.d.ts +14 -2
- package/dist/lib/state.js +51 -20
- package/dist/lib/teams/agents.d.ts +5 -4
- package/dist/lib/teams/agents.js +48 -22
- package/dist/lib/teams/api.d.ts +2 -1
- package/dist/lib/teams/api.js +4 -3
- package/dist/lib/teams/parsers.d.ts +1 -1
- package/dist/lib/teams/parsers.js +153 -3
- package/dist/lib/teams/summarizer.js +18 -2
- package/dist/lib/teams/worktree.js +14 -3
- package/dist/lib/types.d.ts +63 -4
- package/dist/lib/types.js +8 -3
- package/dist/lib/usage.d.ts +27 -2
- package/dist/lib/usage.js +100 -17
- package/dist/lib/versions.d.ts +45 -3
- package/dist/lib/versions.js +455 -60
- package/package.json +15 -14
- package/scripts/install-helper.js +97 -0
- package/scripts/postinstall.js +16 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
- package/npm-shrinkwrap.json +0 -3162
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Agent event stream parsers.
|
|
3
3
|
*
|
|
4
4
|
* Normalizes the heterogeneous JSON event formats emitted by each agent CLI
|
|
5
|
-
* (Claude, Codex, Gemini, Cursor, OpenCode) into a unified
|
|
6
|
-
* with consistent types: init, message, tool_use, bash,
|
|
7
|
-
* file_create, file_delete, result, error, and others.
|
|
5
|
+
* (Claude, Codex, Gemini, Cursor, OpenCode, Grok, Antigravity) into a unified
|
|
6
|
+
* event schema with consistent types: init, message, tool_use, bash,
|
|
7
|
+
* file_read, file_write, file_create, file_delete, result, error, and others.
|
|
8
8
|
*/
|
|
9
9
|
import { extractFileOpsFromBash } from './file_ops.js';
|
|
10
10
|
const claudeToolUseMap = new Map();
|
|
@@ -25,6 +25,12 @@ export function normalizeEvents(agentType, raw) {
|
|
|
25
25
|
else if (agentType === 'opencode') {
|
|
26
26
|
return normalizeOpencode(raw);
|
|
27
27
|
}
|
|
28
|
+
else if (agentType === 'grok') {
|
|
29
|
+
return normalizeGrok(raw);
|
|
30
|
+
}
|
|
31
|
+
else if (agentType === 'antigravity') {
|
|
32
|
+
return normalizeAntigravity(raw);
|
|
33
|
+
}
|
|
28
34
|
const timestamp = new Date().toISOString();
|
|
29
35
|
return [{
|
|
30
36
|
type: raw.type || 'unknown',
|
|
@@ -824,6 +830,150 @@ function normalizeOpencode(raw) {
|
|
|
824
830
|
timestamp: timestamp,
|
|
825
831
|
}];
|
|
826
832
|
}
|
|
833
|
+
// --- Grok parsing ---
|
|
834
|
+
// Grok's streaming-json mode emits one JSON object per token, with three event
|
|
835
|
+
// types:
|
|
836
|
+
// {"type":"thought","data":"<chunk>"} — reasoning tokens (many, small)
|
|
837
|
+
// {"type":"text","data":"<chunk>"} — visible response tokens (many, small)
|
|
838
|
+
// {"type":"end","stopReason":"EndTurn","sessionId":"<uuid>","requestId":"<uuid>"}
|
|
839
|
+
//
|
|
840
|
+
// Tool calls are NOT exposed as separate events in this format; they appear
|
|
841
|
+
// inside the `thought` text as XML-like markup. Extracting them reliably would
|
|
842
|
+
// require running a streaming XML/markup parser over concatenated thought
|
|
843
|
+
// chunks, which is out of scope for v1. The teams summary will show grok
|
|
844
|
+
// teammates' bash/file ops as empty — known limitation, fixable later by
|
|
845
|
+
// switching to grok's `agent` subcommand (richer event stream) once stable.
|
|
846
|
+
//
|
|
847
|
+
// Tokens are emitted as `message` events with `complete: false` so the
|
|
848
|
+
// summarizer can concatenate them into a final message; `thinking` events are
|
|
849
|
+
// already collapsed by the summarizer's groupAndFlattenEvents pathway.
|
|
850
|
+
function normalizeGrok(raw) {
|
|
851
|
+
if (!raw || typeof raw !== 'object') {
|
|
852
|
+
return [{
|
|
853
|
+
type: 'unknown',
|
|
854
|
+
agent: 'grok',
|
|
855
|
+
raw: raw,
|
|
856
|
+
timestamp: new Date().toISOString(),
|
|
857
|
+
}];
|
|
858
|
+
}
|
|
859
|
+
const eventType = raw.type || 'unknown';
|
|
860
|
+
const timestamp = new Date().toISOString();
|
|
861
|
+
if (eventType === 'thought') {
|
|
862
|
+
const data = typeof raw.data === 'string' ? raw.data : '';
|
|
863
|
+
if (!data)
|
|
864
|
+
return [];
|
|
865
|
+
return [{
|
|
866
|
+
type: 'thinking',
|
|
867
|
+
agent: 'grok',
|
|
868
|
+
content: data,
|
|
869
|
+
timestamp: timestamp,
|
|
870
|
+
}];
|
|
871
|
+
}
|
|
872
|
+
if (eventType === 'text') {
|
|
873
|
+
const data = typeof raw.data === 'string' ? raw.data : '';
|
|
874
|
+
if (!data)
|
|
875
|
+
return [];
|
|
876
|
+
return [{
|
|
877
|
+
type: 'message',
|
|
878
|
+
agent: 'grok',
|
|
879
|
+
content: data,
|
|
880
|
+
complete: false,
|
|
881
|
+
timestamp: timestamp,
|
|
882
|
+
}];
|
|
883
|
+
}
|
|
884
|
+
if (eventType === 'end') {
|
|
885
|
+
const stopReason = typeof raw.stopReason === 'string' ? raw.stopReason : '';
|
|
886
|
+
const status = stopReason === 'EndTurn' || stopReason === 'StopSequence' || stopReason === ''
|
|
887
|
+
? 'success'
|
|
888
|
+
: 'error';
|
|
889
|
+
return [{
|
|
890
|
+
type: 'result',
|
|
891
|
+
agent: 'grok',
|
|
892
|
+
status: status,
|
|
893
|
+
stop_reason: stopReason || null,
|
|
894
|
+
session_id: typeof raw.sessionId === 'string' ? raw.sessionId : null,
|
|
895
|
+
timestamp: timestamp,
|
|
896
|
+
}];
|
|
897
|
+
}
|
|
898
|
+
return [{
|
|
899
|
+
type: eventType,
|
|
900
|
+
agent: 'grok',
|
|
901
|
+
raw: raw,
|
|
902
|
+
timestamp: timestamp,
|
|
903
|
+
}];
|
|
904
|
+
}
|
|
905
|
+
// --- Antigravity parsing ---
|
|
906
|
+
// Intentionally conservative. Antigravity's `agy` binary advertises an
|
|
907
|
+
// `--output-format json` flag in its docs, but the released binary errors with
|
|
908
|
+
// `flags provided but not defined: -output-format` (tracked upstream as
|
|
909
|
+
// google-antigravity/antigravity-cli#7, open as of May 2026). Until JSON
|
|
910
|
+
// streaming stabilizes, this parser treats agy output as a black box:
|
|
911
|
+
// - non-object input (a plain string line, or null/number) becomes a single
|
|
912
|
+
// `message` event with the full content and complete:true so the
|
|
913
|
+
// summarizer captures it without token-level concatenation
|
|
914
|
+
// - objects with a recognizable `type` field (e.g. `init`, `message`,
|
|
915
|
+
// `result`) get a minimal shape-preserving normalization
|
|
916
|
+
// - everything else falls through to the generic unknown-event shape
|
|
917
|
+
// Once agy ships stable streaming JSON, replace this with proper event
|
|
918
|
+
// mapping mirroring normalizeGrok / normalizeClaude.
|
|
919
|
+
function normalizeAntigravity(raw) {
|
|
920
|
+
const timestamp = new Date().toISOString();
|
|
921
|
+
if (typeof raw === 'string') {
|
|
922
|
+
if (!raw)
|
|
923
|
+
return [];
|
|
924
|
+
return [{
|
|
925
|
+
type: 'message',
|
|
926
|
+
agent: 'antigravity',
|
|
927
|
+
content: raw,
|
|
928
|
+
complete: true,
|
|
929
|
+
timestamp: timestamp,
|
|
930
|
+
}];
|
|
931
|
+
}
|
|
932
|
+
if (!raw || typeof raw !== 'object') {
|
|
933
|
+
return [{
|
|
934
|
+
type: 'unknown',
|
|
935
|
+
agent: 'antigravity',
|
|
936
|
+
raw: raw,
|
|
937
|
+
timestamp: timestamp,
|
|
938
|
+
}];
|
|
939
|
+
}
|
|
940
|
+
const eventType = raw.type || 'unknown';
|
|
941
|
+
if (eventType === 'init') {
|
|
942
|
+
return [{
|
|
943
|
+
type: 'init',
|
|
944
|
+
agent: 'antigravity',
|
|
945
|
+
session_id: typeof raw.sessionId === 'string' ? raw.sessionId : null,
|
|
946
|
+
timestamp: timestamp,
|
|
947
|
+
}];
|
|
948
|
+
}
|
|
949
|
+
if (eventType === 'message') {
|
|
950
|
+
const content = typeof raw.content === 'string' ? raw.content : '';
|
|
951
|
+
if (!content)
|
|
952
|
+
return [];
|
|
953
|
+
return [{
|
|
954
|
+
type: 'message',
|
|
955
|
+
agent: 'antigravity',
|
|
956
|
+
content: content,
|
|
957
|
+
complete: raw.complete !== false,
|
|
958
|
+
timestamp: timestamp,
|
|
959
|
+
}];
|
|
960
|
+
}
|
|
961
|
+
if (eventType === 'result') {
|
|
962
|
+
return [{
|
|
963
|
+
type: 'result',
|
|
964
|
+
agent: 'antigravity',
|
|
965
|
+
status: raw.status === 'error' ? 'error' : 'success',
|
|
966
|
+
session_id: typeof raw.sessionId === 'string' ? raw.sessionId : null,
|
|
967
|
+
timestamp: timestamp,
|
|
968
|
+
}];
|
|
969
|
+
}
|
|
970
|
+
return [{
|
|
971
|
+
type: eventType,
|
|
972
|
+
agent: 'antigravity',
|
|
973
|
+
raw: raw,
|
|
974
|
+
timestamp: timestamp,
|
|
975
|
+
}];
|
|
976
|
+
}
|
|
827
977
|
/** Parse a single JSONL line into normalized events. Returns null if the line is not valid JSON. */
|
|
828
978
|
export function parseEvent(agentType, line) {
|
|
829
979
|
try {
|
|
@@ -159,11 +159,18 @@ export function groupAndFlattenEvents(events) {
|
|
|
159
159
|
if (eventType === 'message' || eventType === 'thinking') {
|
|
160
160
|
let count = 1;
|
|
161
161
|
let combinedContent = event.content || '';
|
|
162
|
+
// Streaming token events (complete:false) get concatenated without a
|
|
163
|
+
// separator so tokens reassemble into readable prose. Whole-turn events
|
|
164
|
+
// get joined with newlines so distinct turns/thoughts stay separated.
|
|
165
|
+
const isStreaming = event.complete === false;
|
|
162
166
|
let j = i + 1;
|
|
163
167
|
while (j < events.length && events[j].type === eventType) {
|
|
164
168
|
count++;
|
|
165
169
|
if (events[j].content) {
|
|
166
|
-
|
|
170
|
+
const sep = isStreaming || events[j].complete === false
|
|
171
|
+
? ''
|
|
172
|
+
: (combinedContent ? '\n' : '');
|
|
173
|
+
combinedContent += sep + events[j].content;
|
|
167
174
|
}
|
|
168
175
|
j++;
|
|
169
176
|
}
|
|
@@ -405,7 +412,16 @@ export function summarizeEvents(agentId, agentType, status, events, duration = n
|
|
|
405
412
|
else if (eventType === 'message') {
|
|
406
413
|
const content = event.content || '';
|
|
407
414
|
if (content) {
|
|
408
|
-
|
|
415
|
+
// Streaming token-by-token messages (e.g., grok) arrive as many small
|
|
416
|
+
// `message` events with complete:false; concatenate them so the final
|
|
417
|
+
// turn reads as one message. Whole-turn messages (claude, codex,
|
|
418
|
+
// gemini) keep their complete:true semantics — last one wins.
|
|
419
|
+
if (event.complete === false) {
|
|
420
|
+
summary.finalMessage = (summary.finalMessage || '') + content;
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
summary.finalMessage = content;
|
|
424
|
+
}
|
|
409
425
|
}
|
|
410
426
|
}
|
|
411
427
|
else if (eventType === 'error') {
|
|
@@ -8,7 +8,9 @@ import { execFile } from 'child_process';
|
|
|
8
8
|
import { promisify } from 'util';
|
|
9
9
|
import * as fs from 'fs/promises';
|
|
10
10
|
import * as path from 'path';
|
|
11
|
+
import { safeJoin } from '../paths.js';
|
|
11
12
|
const execFileAsync = promisify(execFile);
|
|
13
|
+
const WORKTREE_NAME_RE = /^[A-Za-z0-9_-]+$/;
|
|
12
14
|
export async function isGitRepo(dir) {
|
|
13
15
|
try {
|
|
14
16
|
await execFileAsync('git', ['rev-parse', '--git-dir'], { cwd: dir });
|
|
@@ -42,8 +44,11 @@ export async function hasUncommittedChanges(worktreePath) {
|
|
|
42
44
|
* @returns The absolute path to the created worktree
|
|
43
45
|
*/
|
|
44
46
|
export async function createWorktree(repoDir, worktreeName) {
|
|
47
|
+
if (!WORKTREE_NAME_RE.test(worktreeName)) {
|
|
48
|
+
throw new Error(`Invalid worktree name: ${worktreeName}`);
|
|
49
|
+
}
|
|
45
50
|
const gitRoot = await getGitRoot(repoDir);
|
|
46
|
-
const worktreePath = path.join(gitRoot, '.agents', 'worktrees', worktreeName);
|
|
51
|
+
const worktreePath = safeJoin(path.join(gitRoot, '.agents', 'worktrees'), worktreeName);
|
|
47
52
|
const branchName = `agents/${worktreeName}`;
|
|
48
53
|
await fs.mkdir(path.dirname(worktreePath), { recursive: true });
|
|
49
54
|
await execFileAsync('git', ['worktree', 'add', '-b', branchName, worktreePath, 'HEAD'], {
|
|
@@ -59,8 +64,11 @@ export async function createWorktree(repoDir, worktreeName) {
|
|
|
59
64
|
* @param deleteBranch - Whether to delete the associated branch
|
|
60
65
|
*/
|
|
61
66
|
export async function removeWorktree(repoDir, worktreeName, deleteBranch = true) {
|
|
67
|
+
if (!WORKTREE_NAME_RE.test(worktreeName)) {
|
|
68
|
+
throw new Error(`Invalid worktree name: ${worktreeName}`);
|
|
69
|
+
}
|
|
62
70
|
const gitRoot = await getGitRoot(repoDir);
|
|
63
|
-
const worktreePath = path.join(gitRoot, '.agents', 'worktrees', worktreeName);
|
|
71
|
+
const worktreePath = safeJoin(path.join(gitRoot, '.agents', 'worktrees'), worktreeName);
|
|
64
72
|
const branchName = `agents/${worktreeName}`;
|
|
65
73
|
try {
|
|
66
74
|
await execFileAsync('git', ['worktree', 'remove', '--force', worktreePath], { cwd: gitRoot });
|
|
@@ -86,7 +94,10 @@ export async function removeWorktree(repoDir, worktreeName, deleteBranch = true)
|
|
|
86
94
|
* Get the worktree path for a given name.
|
|
87
95
|
*/
|
|
88
96
|
export function getWorktreePath(gitRoot, worktreeName) {
|
|
89
|
-
|
|
97
|
+
if (!WORKTREE_NAME_RE.test(worktreeName)) {
|
|
98
|
+
throw new Error(`Invalid worktree name: ${worktreeName}`);
|
|
99
|
+
}
|
|
100
|
+
return safeJoin(path.join(gitRoot, '.agents', 'worktrees'), worktreeName);
|
|
90
101
|
}
|
|
91
102
|
/**
|
|
92
103
|
* Get the branch name for a worktree.
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -40,6 +40,12 @@ export interface AgentConfig {
|
|
|
40
40
|
skills: Capability;
|
|
41
41
|
commands: Capability;
|
|
42
42
|
plugins: Capability;
|
|
43
|
+
/**
|
|
44
|
+
* Permission modes this agent natively supports. Modes outside this set
|
|
45
|
+
* are gated by buildExecCommand: `auto` silently degrades to `edit`,
|
|
46
|
+
* `skip` errors with a clear message naming the supported modes.
|
|
47
|
+
*/
|
|
48
|
+
modes: Mode[];
|
|
43
49
|
/**
|
|
44
50
|
* Whether the agent natively resolves `@path/to/file` imports inside its
|
|
45
51
|
* rules file at session start. If false, agents-cli must pre-compile the
|
|
@@ -60,6 +66,19 @@ export type Capability = boolean | {
|
|
|
60
66
|
};
|
|
61
67
|
/** Names of every gateable capability on AgentConfig. */
|
|
62
68
|
export type CapabilityName = 'hooks' | 'mcp' | 'allowlist' | 'skills' | 'commands' | 'plugins';
|
|
69
|
+
/**
|
|
70
|
+
* Permission modes controlling agent autonomy.
|
|
71
|
+
* plan read-only investigation; no writes, no shell side-effects
|
|
72
|
+
* edit may edit files; prompts for shell/risky operations
|
|
73
|
+
* auto smart classifier auto-approves safe operations, prompts for risky ones
|
|
74
|
+
* skip bypasses every permission prompt (dangerously-skip-permissions)
|
|
75
|
+
*
|
|
76
|
+
* `full` is accepted as a permanent silent alias for `skip` via normalizeMode().
|
|
77
|
+
* Per-agent support is declared on AgentConfig.capabilities.modes.
|
|
78
|
+
*/
|
|
79
|
+
export type Mode = 'plan' | 'edit' | 'auto' | 'skip';
|
|
80
|
+
/** Every canonical mode in declaration order. Useful for iteration / validation. */
|
|
81
|
+
export declare const ALL_MODES: readonly Mode[];
|
|
63
82
|
/** Reason a capability check failed. */
|
|
64
83
|
export type CapabilityFailReason = 'unsupported' | 'too_old' | 'too_new';
|
|
65
84
|
/** Result of `supports(agent, cap, version?)`. */
|
|
@@ -100,6 +119,36 @@ export interface HookMatches {
|
|
|
100
119
|
cwd_includes?: string | string[];
|
|
101
120
|
project_has?: string;
|
|
102
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Cache scoping. Determines which cache file a hook invocation reads/writes:
|
|
124
|
+
* - `global` one file per hook, shared across cwds/sessions. Right for
|
|
125
|
+
* SessionStart hooks pulling org-wide context (Linear sprint).
|
|
126
|
+
* - `per-cwd` keyed on the working directory the hook fires from.
|
|
127
|
+
* - `per-session` keyed on the agent's session_id (read from stdin JSON).
|
|
128
|
+
* - `per-project` keyed on the nearest git repo root above cwd.
|
|
129
|
+
*/
|
|
130
|
+
export type HookCacheKey = 'global' | 'per-cwd' | 'per-session' | 'per-project';
|
|
131
|
+
/** Prefetch strategy when the cache is stale. */
|
|
132
|
+
export type HookCachePrefetch = 'none' | 'background';
|
|
133
|
+
/**
|
|
134
|
+
* Full hook cache config. Authors usually use the shorthand string form
|
|
135
|
+
* (`HookCache`) below. Shorthand examples in hooks.yaml:
|
|
136
|
+
*
|
|
137
|
+
* cache: 5m # → { ttl: 300, key: 'global', prefetch: 'none' }
|
|
138
|
+
* cache: 5m-bg # → { ttl: 300, key: 'global', prefetch: 'background' }
|
|
139
|
+
* cache: # full form
|
|
140
|
+
* ttl: 1h
|
|
141
|
+
* key: per-cwd
|
|
142
|
+
* prefetch: background
|
|
143
|
+
*/
|
|
144
|
+
export interface HookCacheConfig {
|
|
145
|
+
/** TTL in seconds or duration string ("30s", "5m", "1h"). */
|
|
146
|
+
ttl: number | string;
|
|
147
|
+
key?: HookCacheKey;
|
|
148
|
+
prefetch?: HookCachePrefetch;
|
|
149
|
+
}
|
|
150
|
+
/** Cache shorthand: duration string, optionally suffixed `-bg` for background prefetch. */
|
|
151
|
+
export type HookCache = string | HookCacheConfig;
|
|
103
152
|
/** Hook entry as declared in a package manifest (agents.yaml). */
|
|
104
153
|
export interface ManifestHook {
|
|
105
154
|
script: string;
|
|
@@ -114,6 +163,13 @@ export interface ManifestHook {
|
|
|
114
163
|
override?: boolean;
|
|
115
164
|
/** Optional pre-filter predicates evaluated before invoking the script. */
|
|
116
165
|
matches?: HookMatches;
|
|
166
|
+
/**
|
|
167
|
+
* Opt-in caching. When set, the registrar generates a per-hook shim
|
|
168
|
+
* under the hook shims dir that handles cache lookup, stale-while-revalidate,
|
|
169
|
+
* and per-invocation timing/logging, then registers that shim with the agent
|
|
170
|
+
* instead of the raw script. The underlying script is unchanged.
|
|
171
|
+
*/
|
|
172
|
+
cache?: HookCache;
|
|
117
173
|
}
|
|
118
174
|
/** Lightweight hook descriptor used in resource listings. */
|
|
119
175
|
export interface HookResourceEntry {
|
|
@@ -203,9 +259,12 @@ export interface RegistryConfig {
|
|
|
203
259
|
/** Built-in registry endpoints shipped with agents-cli. */
|
|
204
260
|
export declare const DEFAULT_REGISTRIES: Record<RegistryType, Record<string, RegistryConfig>>;
|
|
205
261
|
/**
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
262
|
+
* Third-party registries pre-seeded on first install for discoverability.
|
|
263
|
+
*
|
|
264
|
+
* These ship into new users' agents.yaml once, but are not "defaults" — after
|
|
265
|
+
* seeding they behave like any user-added registry (listable, disable-able,
|
|
266
|
+
* removable). Removed users can `agents registry remove <name>` to opt out;
|
|
267
|
+
* once removed they don't come back.
|
|
209
268
|
*/
|
|
210
269
|
export declare const SEEDED_REGISTRIES: Record<RegistryType, Record<string, RegistryConfig>>;
|
|
211
270
|
/** A single installable package within an MCP server entry. */
|
|
@@ -466,7 +525,7 @@ export interface BrowserProfileConfig {
|
|
|
466
525
|
};
|
|
467
526
|
/** Directory holding source-side JSONL logs (e.g. ~/.rush/logs). */
|
|
468
527
|
logDir?: string;
|
|
469
|
-
/** Optional SSH host where logDir lives, e.g. "user@
|
|
528
|
+
/** Optional SSH host where logDir lives, e.g. "user@remote-host". */
|
|
470
529
|
logHost?: string;
|
|
471
530
|
}
|
|
472
531
|
/** Options controlling which agents and resources are synced during `agents pull` / `agents use`. */
|
package/dist/lib/types.js
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* configuration schemas, resource tracking, registry types, and permission
|
|
6
6
|
* formats for each supported agent.
|
|
7
7
|
*/
|
|
8
|
+
/** Every canonical mode in declaration order. Useful for iteration / validation. */
|
|
9
|
+
export const ALL_MODES = ['plan', 'edit', 'auto', 'skip'];
|
|
8
10
|
/** Canonical system repo cloned into ~/.agents-system/. */
|
|
9
11
|
export const DEFAULT_SYSTEM_REPO = 'gh:phnx-labs/.agents-system';
|
|
10
12
|
/** Strip the `gh:` prefix and `.git` suffix to get a GitHub `owner/repo` slug. */
|
|
@@ -22,9 +24,12 @@ export const DEFAULT_REGISTRIES = {
|
|
|
22
24
|
skill: {},
|
|
23
25
|
};
|
|
24
26
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
27
|
+
* Third-party registries pre-seeded on first install for discoverability.
|
|
28
|
+
*
|
|
29
|
+
* These ship into new users' agents.yaml once, but are not "defaults" — after
|
|
30
|
+
* seeding they behave like any user-added registry (listable, disable-able,
|
|
31
|
+
* removable). Removed users can `agents registry remove <name>` to opt out;
|
|
32
|
+
* once removed they don't come back.
|
|
28
33
|
*/
|
|
29
34
|
export const SEEDED_REGISTRIES = {
|
|
30
35
|
mcp: {},
|
package/dist/lib/usage.d.ts
CHANGED
|
@@ -71,12 +71,37 @@ export declare function getUsageInfoByIdentity(inputs: UsageIdentityInput[]): Pr
|
|
|
71
71
|
usageByKey: Map<string, UsageInfo>;
|
|
72
72
|
}>;
|
|
73
73
|
/**
|
|
74
|
-
* Fetch usage for a single identity
|
|
75
|
-
*
|
|
74
|
+
* Fetch usage for a single identity using stale-while-revalidate.
|
|
75
|
+
*
|
|
76
|
+
* - Cache fresh (< 2 min): return cached snapshot, NO network.
|
|
77
|
+
* - Cache stale but < 24h: return cached snapshot instantly, fire background refresh.
|
|
78
|
+
* - Cache too stale or absent: block on live fetch, fall back to cache on error.
|
|
79
|
+
*
|
|
80
|
+
* This keeps `agents run` startup off the network on the hot path. The first
|
|
81
|
+
* invocation after a cold install or 24h gap still blocks once to seed the
|
|
82
|
+
* cache; every run after that returns instantly while the cache silently
|
|
83
|
+
* refreshes in the background.
|
|
76
84
|
*/
|
|
77
85
|
export declare function getUsageInfoForIdentity(input: UsageIdentityInput): Promise<UsageInfo>;
|
|
78
86
|
/** Format a one-line usage summary with compact bars for inline display. */
|
|
79
87
|
export declare function formatUsageSummary(plan: string | null, snapshot: UsageSnapshot | null, planWidth?: number): string;
|
|
88
|
+
/**
|
|
89
|
+
* Compact colored badge for the account's overall usage status. Renders only
|
|
90
|
+
* when the account is throttled — `available` and `null` return ''.
|
|
91
|
+
*
|
|
92
|
+
* - `out_of_credits` → red "out of credits" (terminal account, all buckets dry)
|
|
93
|
+
* - `rate_limited` → yellow "rate-limited" (transient throttling)
|
|
94
|
+
*
|
|
95
|
+
* The badge sits between the usage bars and `lastActive` in `agents view`, so
|
|
96
|
+
* a glance at the row tells the user whether the version can do useful work.
|
|
97
|
+
* The same signal is exposed as `usageStatus` in `agents view --json` for
|
|
98
|
+
* programmatic consumers (e.g. the swarmify panel's "resume in healthy agent").
|
|
99
|
+
*
|
|
100
|
+
* The switch is exhaustive on purpose — adding a new `AccountInfo.usageStatus`
|
|
101
|
+
* value without updating the cases here is a build error at `_exhaustive`,
|
|
102
|
+
* which is exactly the bug class this PR is fixing.
|
|
103
|
+
*/
|
|
104
|
+
export declare function formatUsageStatusBadge(usageStatus: 'available' | 'rate_limited' | 'out_of_credits' | null | undefined): string;
|
|
80
105
|
/** Format a multi-line usage section for detailed agent views. */
|
|
81
106
|
export declare function formatUsageSection(usage: UsageInfo): string[];
|
|
82
107
|
/** Load Claude OAuth credentials from the system keychain/keyring. */
|
package/dist/lib/usage.js
CHANGED
|
@@ -96,42 +96,95 @@ export async function getUsageInfoByIdentity(inputs) {
|
|
|
96
96
|
usageByKey: new Map(usageResults.map(({ key, usage }) => [key, usage])),
|
|
97
97
|
};
|
|
98
98
|
}
|
|
99
|
-
const USAGE_CACHE_FRESH_MS = 2 * 60 * 1000; // 2 minutes
|
|
99
|
+
const USAGE_CACHE_FRESH_MS = 2 * 60 * 1000; // 2 minutes — fresh window: don't refresh.
|
|
100
|
+
const USAGE_CACHE_SWR_MS = 24 * 60 * 60 * 1000; // 24 hours — beyond this, block on live fetch.
|
|
101
|
+
/** In-process dedup: don't fire concurrent background refreshes for the same identity. */
|
|
102
|
+
const inFlightRefreshes = new Map();
|
|
100
103
|
/**
|
|
101
|
-
* Fetch usage for a single identity
|
|
102
|
-
*
|
|
104
|
+
* Fetch usage for a single identity using stale-while-revalidate.
|
|
105
|
+
*
|
|
106
|
+
* - Cache fresh (< 2 min): return cached snapshot, NO network.
|
|
107
|
+
* - Cache stale but < 24h: return cached snapshot instantly, fire background refresh.
|
|
108
|
+
* - Cache too stale or absent: block on live fetch, fall back to cache on error.
|
|
109
|
+
*
|
|
110
|
+
* This keeps `agents run` startup off the network on the hot path. The first
|
|
111
|
+
* invocation after a cold install or 24h gap still blocks once to seed the
|
|
112
|
+
* cache; every run after that returns instantly while the cache silently
|
|
113
|
+
* refreshes in the background.
|
|
103
114
|
*/
|
|
104
115
|
export async function getUsageInfoForIdentity(input) {
|
|
105
116
|
const usageKey = getUsageLookupKey(input.info);
|
|
106
|
-
//
|
|
107
|
-
if (input.agentId
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
}
|
|
117
|
+
// Non-Claude or no identity: legacy path, blocking fetch.
|
|
118
|
+
if (input.agentId !== 'claude' || !usageKey) {
|
|
119
|
+
return getUsageInfo(input.agentId, {
|
|
120
|
+
home: input.home,
|
|
121
|
+
cliVersion: input.cliVersion,
|
|
122
|
+
organizationId: input.info.organizationId,
|
|
123
|
+
});
|
|
115
124
|
}
|
|
116
|
-
|
|
125
|
+
const cached = readClaudeUsageCache(usageKey);
|
|
126
|
+
const ageMs = cached?.capturedAt ? Date.now() - cached.capturedAt.getTime() : Infinity;
|
|
127
|
+
// Fresh: cache is recent enough, skip network entirely.
|
|
128
|
+
if (cached && ageMs < USAGE_CACHE_FRESH_MS) {
|
|
129
|
+
return { snapshot: cached, error: null };
|
|
130
|
+
}
|
|
131
|
+
// Stale-while-revalidate: cache exists and isn't ancient, return it now and
|
|
132
|
+
// refresh in the background so the next invocation has fresh data.
|
|
133
|
+
if (cached && ageMs < USAGE_CACHE_SWR_MS) {
|
|
134
|
+
triggerBackgroundUsageRefresh(input, usageKey);
|
|
135
|
+
return { snapshot: cached, error: null };
|
|
136
|
+
}
|
|
137
|
+
// Cold cache or > 24h old: block on live fetch.
|
|
117
138
|
const usage = await getUsageInfo(input.agentId, {
|
|
118
139
|
home: input.home,
|
|
119
140
|
cliVersion: input.cliVersion,
|
|
120
141
|
organizationId: input.info.organizationId,
|
|
121
142
|
});
|
|
122
|
-
if (input.agentId !== 'claude' || !usageKey) {
|
|
123
|
-
return usage;
|
|
124
|
-
}
|
|
125
143
|
if (usage.snapshot?.source === 'live') {
|
|
126
144
|
writeClaudeUsageCache(usageKey, usage.snapshot);
|
|
127
145
|
return usage;
|
|
128
146
|
}
|
|
129
|
-
|
|
147
|
+
// Live fetch failed — last-resort fallback to whatever cache we had.
|
|
130
148
|
if (cached) {
|
|
131
149
|
return { snapshot: cached, error: usage.error };
|
|
132
150
|
}
|
|
133
151
|
return usage;
|
|
134
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* Kick off a background refresh of the usage cache. Errors are swallowed —
|
|
155
|
+
* a failed background refresh leaves the existing cache in place for the
|
|
156
|
+
* next invocation. The work is deferred to a future event-loop tick via
|
|
157
|
+
* `setImmediate` because some of the call chain (loadClaudeOauth →
|
|
158
|
+
* getKeychainToken → execFileSync) does synchronous I/O even though the
|
|
159
|
+
* functions are declared `async`. Without the defer, that sync I/O blocks
|
|
160
|
+
* the SWR caller and defeats the whole point of returning the cache instantly.
|
|
161
|
+
*/
|
|
162
|
+
function triggerBackgroundUsageRefresh(input, usageKey) {
|
|
163
|
+
if (inFlightRefreshes.has(usageKey))
|
|
164
|
+
return;
|
|
165
|
+
const promise = new Promise((resolve) => {
|
|
166
|
+
setImmediate(async () => {
|
|
167
|
+
try {
|
|
168
|
+
const usage = await getUsageInfo(input.agentId, {
|
|
169
|
+
home: input.home,
|
|
170
|
+
cliVersion: input.cliVersion,
|
|
171
|
+
organizationId: input.info.organizationId,
|
|
172
|
+
});
|
|
173
|
+
if (usage.snapshot?.source === 'live') {
|
|
174
|
+
writeClaudeUsageCache(usageKey, usage.snapshot);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
/* background refresh failed — leave existing cache in place */
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
inFlightRefreshes.delete(usageKey);
|
|
182
|
+
resolve();
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
inFlightRefreshes.set(usageKey, promise);
|
|
187
|
+
}
|
|
135
188
|
/** Format a one-line usage summary with compact bars for inline display. */
|
|
136
189
|
export function formatUsageSummary(plan, snapshot, planWidth = 3) {
|
|
137
190
|
const parts = [];
|
|
@@ -148,6 +201,36 @@ export function formatUsageSummary(plan, snapshot, planWidth = 3) {
|
|
|
148
201
|
}
|
|
149
202
|
return parts.join(' ');
|
|
150
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* Compact colored badge for the account's overall usage status. Renders only
|
|
206
|
+
* when the account is throttled — `available` and `null` return ''.
|
|
207
|
+
*
|
|
208
|
+
* - `out_of_credits` → red "out of credits" (terminal account, all buckets dry)
|
|
209
|
+
* - `rate_limited` → yellow "rate-limited" (transient throttling)
|
|
210
|
+
*
|
|
211
|
+
* The badge sits between the usage bars and `lastActive` in `agents view`, so
|
|
212
|
+
* a glance at the row tells the user whether the version can do useful work.
|
|
213
|
+
* The same signal is exposed as `usageStatus` in `agents view --json` for
|
|
214
|
+
* programmatic consumers (e.g. the swarmify panel's "resume in healthy agent").
|
|
215
|
+
*
|
|
216
|
+
* The switch is exhaustive on purpose — adding a new `AccountInfo.usageStatus`
|
|
217
|
+
* value without updating the cases here is a build error at `_exhaustive`,
|
|
218
|
+
* which is exactly the bug class this PR is fixing.
|
|
219
|
+
*/
|
|
220
|
+
export function formatUsageStatusBadge(usageStatus) {
|
|
221
|
+
if (usageStatus === null || usageStatus === undefined)
|
|
222
|
+
return '';
|
|
223
|
+
switch (usageStatus) {
|
|
224
|
+
case 'available': return '';
|
|
225
|
+
case 'out_of_credits': return chalk.red('out of credits');
|
|
226
|
+
case 'rate_limited': return chalk.yellow('rate-limited');
|
|
227
|
+
default: {
|
|
228
|
+
const _exhaustive = usageStatus;
|
|
229
|
+
void _exhaustive;
|
|
230
|
+
return '';
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
151
234
|
/** Format a multi-line usage section for detailed agent views. */
|
|
152
235
|
export function formatUsageSection(usage) {
|
|
153
236
|
if (!usage.snapshot && !usage.error) {
|