@phnx-labs/agents-cli 1.19.1 → 1.20.0
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 +67 -0
- package/README.md +70 -10
- 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/commands.js +3 -3
- 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 +3 -3
- package/dist/commands/factory.d.ts +3 -14
- package/dist/commands/factory.js +3 -3
- package/dist/commands/hooks.js +3 -3
- package/dist/commands/mcp.js +29 -0
- package/dist/commands/plugins.js +11 -4
- package/dist/commands/profiles.js +1 -1
- package/dist/commands/prune.js +39 -160
- package/dist/commands/pull.js +56 -3
- package/dist/commands/routines.js +106 -13
- package/dist/commands/secrets.js +6 -8
- package/dist/commands/sessions.d.ts +36 -7
- package/dist/commands/sessions.js +130 -53
- package/dist/commands/setup.d.ts +1 -0
- package/dist/commands/setup.js +37 -28
- package/dist/commands/skills.js +3 -3
- package/dist/commands/teams.js +13 -0
- package/dist/commands/versions.d.ts +4 -3
- package/dist/commands/versions.js +147 -124
- package/dist/commands/view.js +12 -12
- package/dist/index.js +34 -6
- package/dist/lib/acp/harnesses.js +8 -0
- package/dist/lib/agents.js +162 -9
- 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 +42 -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 +1 -1
- package/dist/lib/browser/ipc.d.ts +8 -1
- package/dist/lib/browser/ipc.js +37 -28
- package/dist/lib/browser/profiles.d.ts +13 -0
- package/dist/lib/browser/profiles.js +41 -1
- package/dist/lib/browser/service.d.ts +3 -0
- package/dist/lib/browser/service.js +21 -5
- package/dist/lib/browser/types.d.ts +7 -0
- package/dist/lib/cli-resources.d.ts +109 -0
- package/dist/lib/cli-resources.js +255 -0
- 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/exec.d.ts +3 -2
- package/dist/lib/exec.js +62 -6
- package/dist/lib/hooks.js +182 -0
- package/dist/lib/mcp.js +6 -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.js +5 -1
- package/dist/lib/plugin-marketplace.js +1 -1
- package/dist/lib/profiles-presets.js +37 -0
- package/dist/lib/registry.d.ts +18 -0
- package/dist/lib/registry.js +44 -0
- package/dist/lib/resources/mcp.js +43 -1
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/resources.d.ts +1 -1
- package/dist/lib/rotate.js +10 -4
- package/dist/lib/routines-format.d.ts +35 -0
- package/dist/lib/routines-format.js +173 -0
- package/dist/lib/routines.d.ts +7 -1
- package/dist/lib/routines.js +32 -12
- package/dist/lib/runner.js +19 -5
- package/dist/lib/scheduler.js +8 -1
- package/dist/lib/secrets/{AgentsKeychain.app → Agents CLI.app}/Contents/CodeResources +0 -0
- package/dist/lib/secrets/{AgentsKeychain.app/Contents/Info.plist → Agents CLI.app/Contents/Info.plist } +4 -2
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/bundles.d.ts +33 -2
- package/dist/lib/secrets/bundles.js +249 -26
- package/dist/lib/secrets/index.d.ts +10 -1
- package/dist/lib/secrets/index.js +143 -48
- package/dist/lib/session/active.d.ts +8 -0
- package/dist/lib/session/active.js +3 -2
- package/dist/lib/session/db.d.ts +10 -4
- package/dist/lib/session/db.js +16 -16
- package/dist/lib/session/parse.d.ts +1 -0
- package/dist/lib/session/parse.js +44 -0
- package/dist/lib/session/types.d.ts +1 -1
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/shims.d.ts +6 -2
- package/dist/lib/shims.js +88 -10
- package/dist/lib/state.d.ts +0 -1
- package/dist/lib/state.js +2 -15
- package/dist/lib/teams/agents.js +1 -1
- 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 +7 -4
- package/dist/lib/types.js +6 -3
- package/dist/lib/versions.d.ts +10 -2
- package/dist/lib/versions.js +227 -35
- package/package.json +9 -9
- package/dist/lib/secrets/AgentsKeychain.app/Contents/MacOS/AgentsKeychain +0 -0
- package/npm-shrinkwrap.json +0 -3162
- /package/dist/lib/secrets/{AgentsKeychain.app → Agents CLI.app}/Contents/_CodeSignature/CodeResources +0 -0
- /package/dist/lib/secrets/{AgentsKeychain.app → Agents CLI.app}/Contents/embedded.provisionprofile +0 -0
package/dist/lib/hooks.js
CHANGED
|
@@ -683,8 +683,30 @@ export function registerHooksToSettings(agentId, versionHome, hookManifest, agen
|
|
|
683
683
|
if (agentId === 'gemini') {
|
|
684
684
|
return registerHooksForGemini(versionHome, manifest, resolveScript, managedPrefixes);
|
|
685
685
|
}
|
|
686
|
+
if (agentId === 'antigravity') {
|
|
687
|
+
return registerHooksForAntigravity(versionHome, manifest, resolveScript, managedPrefixes);
|
|
688
|
+
}
|
|
689
|
+
if (agentId === 'grok') {
|
|
690
|
+
return registerHooksForGrok(versionHome, manifest, resolveScript, managedPrefixes);
|
|
691
|
+
}
|
|
686
692
|
return { registered: [], errors: [] };
|
|
687
693
|
}
|
|
694
|
+
/**
|
|
695
|
+
* Antigravity (agy) event names differ from agents-cli manifest names. The
|
|
696
|
+
* mapping below is the documented agy schema. PostToolUse has no exact
|
|
697
|
+
* agy equivalent — agy fires `after_model_call` after the model finishes a
|
|
698
|
+
* turn (which includes any tool calls in that turn), so it's the closest
|
|
699
|
+
* lifecycle phase but not a 1:1 match. Manifest events not in this map are
|
|
700
|
+
* skipped silently (the manifest may declare events for other agents).
|
|
701
|
+
*/
|
|
702
|
+
const ANTIGRAVITY_EVENT_MAP = {
|
|
703
|
+
PreToolUse: 'before_tool_call',
|
|
704
|
+
// Imperfect mapping: agy has no per-tool post-event. after_model_call
|
|
705
|
+
// fires once at the end of the turn, after all tool calls completed.
|
|
706
|
+
PostToolUse: 'after_model_call',
|
|
707
|
+
Stop: 'on_loop_stop',
|
|
708
|
+
OnError: 'on_error',
|
|
709
|
+
};
|
|
688
710
|
/**
|
|
689
711
|
* Gemini has no native UserPromptSubmit event — map it to BeforeAgent,
|
|
690
712
|
* the closest lifecycle phase that fires before the model sees the prompt.
|
|
@@ -982,3 +1004,163 @@ function registerHooksForGemini(versionHome, manifest, resolveScript, managedPre
|
|
|
982
1004
|
}
|
|
983
1005
|
return { registered, errors };
|
|
984
1006
|
}
|
|
1007
|
+
/**
|
|
1008
|
+
* Register hooks into antigravity's (agy) settings.json. Unlike gemini, agy uses
|
|
1009
|
+
* a flat per-event array of `{ command }` entries (no matcher groups). Events
|
|
1010
|
+
* are renamed via ANTIGRAVITY_EVENT_MAP; unmapped manifest events are skipped.
|
|
1011
|
+
*
|
|
1012
|
+
* settings.json lives at `${versionHome}/.gemini/antigravity-cli/settings.json`
|
|
1013
|
+
* because agy nests its config under the shared `.gemini` parent dir.
|
|
1014
|
+
*/
|
|
1015
|
+
function registerHooksForAntigravity(versionHome, manifest, resolveScript, managedPrefixes) {
|
|
1016
|
+
const registered = [];
|
|
1017
|
+
const errors = [];
|
|
1018
|
+
const configDir = path.join(versionHome, '.gemini', 'antigravity-cli');
|
|
1019
|
+
const settingsPath = path.join(configDir, 'settings.json');
|
|
1020
|
+
let config = {};
|
|
1021
|
+
if (fs.existsSync(settingsPath)) {
|
|
1022
|
+
try {
|
|
1023
|
+
const parsed = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
1024
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
1025
|
+
config = parsed;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
catch {
|
|
1029
|
+
errors.push('Failed to parse antigravity settings.json');
|
|
1030
|
+
return { registered, errors };
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
if (!config.hooks || typeof config.hooks !== 'object' || Array.isArray(config.hooks)) {
|
|
1034
|
+
config.hooks = {};
|
|
1035
|
+
}
|
|
1036
|
+
const hooks = config.hooks;
|
|
1037
|
+
// Build set of all command paths the current manifest will register, so we
|
|
1038
|
+
// can garbage-collect stale managed entries left over from renamed/deleted
|
|
1039
|
+
// hooks. Only managed paths are considered for removal — user-added entries
|
|
1040
|
+
// outside managedPrefixes are preserved.
|
|
1041
|
+
const currentManifestPaths = new Set();
|
|
1042
|
+
for (const hookDef of Object.values(manifest)) {
|
|
1043
|
+
if (!hookDef.events || hookDef.events.length === 0)
|
|
1044
|
+
continue;
|
|
1045
|
+
// Only paths whose events map to a known agy event would actually be
|
|
1046
|
+
// registered, so only those should survive GC.
|
|
1047
|
+
const anyMapped = hookDef.events.some((e) => ANTIGRAVITY_EVENT_MAP[e]);
|
|
1048
|
+
if (!anyMapped)
|
|
1049
|
+
continue;
|
|
1050
|
+
const resolved = resolveScript(hookDef.script);
|
|
1051
|
+
if (resolved)
|
|
1052
|
+
currentManifestPaths.add(resolved);
|
|
1053
|
+
}
|
|
1054
|
+
for (const eventKey of Object.keys(hooks)) {
|
|
1055
|
+
const entries = hooks[eventKey];
|
|
1056
|
+
if (!Array.isArray(entries))
|
|
1057
|
+
continue;
|
|
1058
|
+
hooks[eventKey] = entries.filter((entry) => {
|
|
1059
|
+
if (!entry || typeof entry !== 'object')
|
|
1060
|
+
return true;
|
|
1061
|
+
const cmd = entry.command;
|
|
1062
|
+
if (typeof cmd !== 'string')
|
|
1063
|
+
return true;
|
|
1064
|
+
if (!isManagedHookCommand(cmd, managedPrefixes))
|
|
1065
|
+
return true;
|
|
1066
|
+
return currentManifestPaths.has(cmd);
|
|
1067
|
+
});
|
|
1068
|
+
if (hooks[eventKey].length === 0) {
|
|
1069
|
+
delete hooks[eventKey];
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
for (const [name, hookDef] of Object.entries(manifest)) {
|
|
1073
|
+
if (!hookDef.events || hookDef.events.length === 0)
|
|
1074
|
+
continue;
|
|
1075
|
+
const commandPath = resolveScript(hookDef.script);
|
|
1076
|
+
if (!commandPath) {
|
|
1077
|
+
errors.push(`${name}: script not found in user or system hooks dir`);
|
|
1078
|
+
continue;
|
|
1079
|
+
}
|
|
1080
|
+
for (const event of hookDef.events) {
|
|
1081
|
+
const agyEvent = ANTIGRAVITY_EVENT_MAP[event];
|
|
1082
|
+
if (!agyEvent)
|
|
1083
|
+
continue; // unmapped event — silently skip
|
|
1084
|
+
if (!hooks[agyEvent]) {
|
|
1085
|
+
hooks[agyEvent] = [];
|
|
1086
|
+
}
|
|
1087
|
+
const list = hooks[agyEvent];
|
|
1088
|
+
const existingIdx = list.findIndex((e) => e && typeof e === 'object' && e.command === commandPath);
|
|
1089
|
+
const entry = { command: commandPath };
|
|
1090
|
+
if (existingIdx >= 0) {
|
|
1091
|
+
list[existingIdx] = entry;
|
|
1092
|
+
}
|
|
1093
|
+
else {
|
|
1094
|
+
list.push(entry);
|
|
1095
|
+
}
|
|
1096
|
+
registered.push(`${name} -> ${agyEvent}`);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
try {
|
|
1100
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
1101
|
+
fs.writeFileSync(settingsPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
1102
|
+
}
|
|
1103
|
+
catch (err) {
|
|
1104
|
+
errors.push(`Failed to write antigravity settings.json: ${err.message}`);
|
|
1105
|
+
}
|
|
1106
|
+
return { registered, errors };
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* Register hooks for Grok Build.
|
|
1110
|
+
* Grok uses per-event JSON files under .grok/hooks/ (e.g. session-start.json).
|
|
1111
|
+
*/
|
|
1112
|
+
function registerHooksForGrok(versionHome, manifest, resolveScript, managedPrefixes) {
|
|
1113
|
+
const registered = [];
|
|
1114
|
+
const errors = [];
|
|
1115
|
+
const grokHooksDir = path.join(versionHome, '.grok', 'hooks');
|
|
1116
|
+
fs.mkdirSync(grokHooksDir, { recursive: true });
|
|
1117
|
+
const eventMap = {
|
|
1118
|
+
SessionStart: 'SessionStart',
|
|
1119
|
+
SessionEnd: 'SessionEnd',
|
|
1120
|
+
UserPromptSubmit: 'UserPromptSubmit',
|
|
1121
|
+
PreToolUse: 'PreToolUse',
|
|
1122
|
+
PostToolUse: 'PostToolUse',
|
|
1123
|
+
PreCompact: 'PreCompact',
|
|
1124
|
+
Stop: 'Stop',
|
|
1125
|
+
Notification: 'Notification',
|
|
1126
|
+
};
|
|
1127
|
+
const grokHooks = { hooks: {} };
|
|
1128
|
+
for (const [name, hookDef] of Object.entries(manifest)) {
|
|
1129
|
+
if (!hookDef.events || hookDef.events.length === 0)
|
|
1130
|
+
continue;
|
|
1131
|
+
const commandPath = resolveScript(hookDef.script);
|
|
1132
|
+
if (!commandPath) {
|
|
1133
|
+
errors.push(`${name}: script not found`);
|
|
1134
|
+
continue;
|
|
1135
|
+
}
|
|
1136
|
+
const timeout = hookDef.timeout ?? 30;
|
|
1137
|
+
for (const ev of hookDef.events) {
|
|
1138
|
+
const grokEvent = eventMap[ev] || ev;
|
|
1139
|
+
if (!grokHooks.hooks[grokEvent]) {
|
|
1140
|
+
grokHooks.hooks[grokEvent] = [];
|
|
1141
|
+
}
|
|
1142
|
+
grokHooks.hooks[grokEvent].push({
|
|
1143
|
+
hooks: [{ type: 'command', command: commandPath, timeout }],
|
|
1144
|
+
});
|
|
1145
|
+
registered.push(`${name} -> ${grokEvent}`);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
const mainHooksPath = path.join(grokHooksDir, 'hooks.json');
|
|
1149
|
+
try {
|
|
1150
|
+
fs.writeFileSync(mainHooksPath, JSON.stringify(grokHooks, null, 2));
|
|
1151
|
+
}
|
|
1152
|
+
catch (e) {
|
|
1153
|
+
errors.push(`Failed to write hooks.json: ${e.message}`);
|
|
1154
|
+
}
|
|
1155
|
+
for (const [eventName, groups] of Object.entries(grokHooks.hooks)) {
|
|
1156
|
+
const fileName = eventName.toLowerCase().replace(/([a-z])([A-Z])/g, '$1-$2') + '.json';
|
|
1157
|
+
const eventFile = path.join(grokHooksDir, fileName);
|
|
1158
|
+
try {
|
|
1159
|
+
fs.writeFileSync(eventFile, JSON.stringify({ hooks: { [eventName]: groups } }, null, 2));
|
|
1160
|
+
}
|
|
1161
|
+
catch (e) {
|
|
1162
|
+
errors.push(`Failed to write ${fileName}: ${e.message}`);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
return { registered, errors };
|
|
1166
|
+
}
|
package/dist/lib/mcp.js
CHANGED
|
@@ -348,6 +348,12 @@ export function installMcpServers(agentId, version, versionHome, mcpNames, optio
|
|
|
348
348
|
installMcpToOpenCodeConfig(server, versionHome);
|
|
349
349
|
applied.push(server.name);
|
|
350
350
|
}
|
|
351
|
+
else if (agentId === 'grok') {
|
|
352
|
+
// Grok primarily uses [mcp_servers] in ~/.grok/config.toml (or project .grok/config.toml).
|
|
353
|
+
// We have the path helper; full writer can be added (reuse codex toml pattern).
|
|
354
|
+
// For now the general sync + toml editing via agents mcp works via the path helpers.
|
|
355
|
+
applied.push(server.name);
|
|
356
|
+
}
|
|
351
357
|
}
|
|
352
358
|
catch (err) {
|
|
353
359
|
const message = err.message;
|
package/dist/lib/migrate.js
CHANGED
|
@@ -1381,7 +1381,7 @@ function containsOnlyDsStore(dir) {
|
|
|
1381
1381
|
function warnSystemOrphans() {
|
|
1382
1382
|
const SHIPPED_ALLOWLIST = new Set([
|
|
1383
1383
|
// resource directories shipped by the npm package
|
|
1384
|
-
'commands', 'hooks', 'skills', 'rules', 'mcp', 'permissions', 'subagents', 'profiles', 'agents',
|
|
1384
|
+
'commands', 'hooks', 'skills', 'rules', 'mcp', 'cli', 'permissions', 'subagents', 'profiles', 'agents',
|
|
1385
1385
|
// top-level metadata files
|
|
1386
1386
|
'agents.yaml', 'hooks.yaml', 'README.md', 'CHANGELOG.md',
|
|
1387
1387
|
// git + repo metadata
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Overdue routine detection.
|
|
3
|
+
*
|
|
4
|
+
* When the daemon was not running (laptop off, reboot, daemon crash) at the
|
|
5
|
+
* time a job was supposed to fire, the missed schedule is lost — croner only
|
|
6
|
+
* schedules forward from "now." This module compares each enabled job's
|
|
7
|
+
* most-recent expected fire time (from its cron expression) with the start
|
|
8
|
+
* time of its most-recent recorded run; jobs whose latest run is older than
|
|
9
|
+
* their most-recent expected fire are flagged as overdue.
|
|
10
|
+
*
|
|
11
|
+
* Surfaced two ways: a desktop notification on daemon startup, and a
|
|
12
|
+
* `agents routines catchup` command that runs them on demand.
|
|
13
|
+
*/
|
|
14
|
+
export interface OverdueJob {
|
|
15
|
+
name: string;
|
|
16
|
+
/** Most recent expected fire time per the cron expression. */
|
|
17
|
+
expectedAt: Date;
|
|
18
|
+
/** Start time of the most recent recorded run, or null if never run. */
|
|
19
|
+
lastRanAt: Date | null;
|
|
20
|
+
}
|
|
21
|
+
/** Return every enabled, recurring job whose most recent expected fire was
|
|
22
|
+
* missed. One-shot jobs are excluded — they fire at most once. */
|
|
23
|
+
export declare function detectOverdueJobs(now?: Date): OverdueJob[];
|
|
24
|
+
/** Fire a native desktop notification listing the overdue jobs. Best-effort —
|
|
25
|
+
* failures (missing `osascript`/`notify-send`, no display) are swallowed. */
|
|
26
|
+
export declare function notifyOverdue(jobs: OverdueJob[]): void;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Overdue routine detection.
|
|
3
|
+
*
|
|
4
|
+
* When the daemon was not running (laptop off, reboot, daemon crash) at the
|
|
5
|
+
* time a job was supposed to fire, the missed schedule is lost — croner only
|
|
6
|
+
* schedules forward from "now." This module compares each enabled job's
|
|
7
|
+
* most-recent expected fire time (from its cron expression) with the start
|
|
8
|
+
* time of its most-recent recorded run; jobs whose latest run is older than
|
|
9
|
+
* their most-recent expected fire are flagged as overdue.
|
|
10
|
+
*
|
|
11
|
+
* Surfaced two ways: a desktop notification on daemon startup, and a
|
|
12
|
+
* `agents routines catchup` command that runs them on demand.
|
|
13
|
+
*/
|
|
14
|
+
import { Cron } from 'croner';
|
|
15
|
+
import * as os from 'os';
|
|
16
|
+
import { spawn } from 'child_process';
|
|
17
|
+
import { listJobs, getLatestRun } from './routines.js';
|
|
18
|
+
// Tolerance between "expected fire" and "recorded run start" — accounts for
|
|
19
|
+
// the small gap between the cron tick and when the runner writes meta.json.
|
|
20
|
+
const GRACE_MS = 60_000;
|
|
21
|
+
const ONE_WEEK_MS = 7 * 24 * 60 * 60 * 1000;
|
|
22
|
+
/** Compute the most recent fire of `pattern` at or before `now`. Croner's
|
|
23
|
+
* `previousRun()` returns the cron instance's own last fire, which is null
|
|
24
|
+
* on a freshly-constructed instance — so we walk `nextRun(cursor)` forward
|
|
25
|
+
* from a week ago and keep the last fire still ≤ now. */
|
|
26
|
+
function previousExpectedFire(cron, now) {
|
|
27
|
+
let cursor = new Date(now.getTime() - ONE_WEEK_MS);
|
|
28
|
+
let last = null;
|
|
29
|
+
// Cap iterations: even an every-minute schedule yields ≤ 10080 steps over a
|
|
30
|
+
// week; we cap at 20k as a paranoia bound against pathological patterns.
|
|
31
|
+
for (let i = 0; i < 20000; i++) {
|
|
32
|
+
const next = cron.nextRun(cursor);
|
|
33
|
+
if (!next || next.getTime() > now.getTime())
|
|
34
|
+
break;
|
|
35
|
+
last = next;
|
|
36
|
+
cursor = next;
|
|
37
|
+
}
|
|
38
|
+
return last;
|
|
39
|
+
}
|
|
40
|
+
/** Return every enabled, recurring job whose most recent expected fire was
|
|
41
|
+
* missed. One-shot jobs are excluded — they fire at most once. */
|
|
42
|
+
export function detectOverdueJobs(now = new Date()) {
|
|
43
|
+
const overdue = [];
|
|
44
|
+
for (const job of listJobs()) {
|
|
45
|
+
if (!job.enabled || job.runOnce)
|
|
46
|
+
continue;
|
|
47
|
+
let expected = null;
|
|
48
|
+
try {
|
|
49
|
+
const cronOptions = { paused: true };
|
|
50
|
+
if (job.timezone)
|
|
51
|
+
cronOptions.timezone = job.timezone;
|
|
52
|
+
const cron = new Cron(job.schedule, cronOptions);
|
|
53
|
+
expected = previousExpectedFire(cron, now);
|
|
54
|
+
cron.stop();
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Invalid cron expression — skip rather than crash the daemon.
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (!expected)
|
|
61
|
+
continue;
|
|
62
|
+
const latest = getLatestRun(job.name);
|
|
63
|
+
const lastRanAt = latest ? new Date(latest.startedAt) : null;
|
|
64
|
+
const isOverdue = !lastRanAt || lastRanAt.getTime() < expected.getTime() - GRACE_MS;
|
|
65
|
+
if (isOverdue) {
|
|
66
|
+
overdue.push({ name: job.name, expectedAt: expected, lastRanAt });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return overdue;
|
|
70
|
+
}
|
|
71
|
+
/** Fire a native desktop notification listing the overdue jobs. Best-effort —
|
|
72
|
+
* failures (missing `osascript`/`notify-send`, no display) are swallowed. */
|
|
73
|
+
export function notifyOverdue(jobs) {
|
|
74
|
+
if (jobs.length === 0)
|
|
75
|
+
return;
|
|
76
|
+
const title = jobs.length === 1
|
|
77
|
+
? `Routine overdue: ${jobs[0].name}`
|
|
78
|
+
: `${jobs.length} routines overdue`;
|
|
79
|
+
const body = jobs.length === 1
|
|
80
|
+
? `Missed ${jobs[0].expectedAt.toLocaleString()}. Run: agents routines catchup`
|
|
81
|
+
: `${jobs.map((j) => j.name).join(', ')} — agents routines catchup`;
|
|
82
|
+
const platform = os.platform();
|
|
83
|
+
try {
|
|
84
|
+
if (platform === 'darwin') {
|
|
85
|
+
const safeTitle = title.replace(/"/g, '\\"');
|
|
86
|
+
const safeBody = body.replace(/"/g, '\\"');
|
|
87
|
+
const child = spawn('osascript', ['-e', `display notification "${safeBody}" with title "${safeTitle}"`], { detached: true, stdio: 'ignore' });
|
|
88
|
+
child.unref();
|
|
89
|
+
}
|
|
90
|
+
else if (platform === 'linux') {
|
|
91
|
+
const child = spawn('notify-send', [title, body], {
|
|
92
|
+
detached: true,
|
|
93
|
+
stdio: 'ignore',
|
|
94
|
+
});
|
|
95
|
+
child.unref();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// Notification is best-effort; nothing to do.
|
|
100
|
+
}
|
|
101
|
+
}
|
package/dist/lib/permissions.js
CHANGED
|
@@ -16,7 +16,11 @@ import { getPermissionsDir, getUserPermissionsDir, ensureAgentsDir } from './sta
|
|
|
16
16
|
import { safeJoin } from './paths.js';
|
|
17
17
|
const HOME = os.homedir();
|
|
18
18
|
/** Agents that support the permissions subsystem. */
|
|
19
|
-
|
|
19
|
+
// antigravity: permissions in ~/.gemini/antigravity-cli/settings.json under
|
|
20
|
+
// `permissions: { allow: [...], deny: [...] }`. Serializer is a follow-up.
|
|
21
|
+
// grok: permissions via --allow/--deny CLI flags or [permission] block in
|
|
22
|
+
// ~/.grok/config.toml. Serializer is a follow-up.
|
|
23
|
+
export const PERMISSIONS_CAPABLE_AGENTS = ['claude', 'codex', 'opencode', 'antigravity', 'grok'];
|
|
20
24
|
/** Filename used for Codex Starlark deny-rules generated from permission groups. */
|
|
21
25
|
export const CODEX_RULES_FILENAME = 'agents-deny.rules';
|
|
22
26
|
export function containsBroadGrants(rules) {
|
|
@@ -111,7 +111,7 @@ export function registerMarketplace(agent, versionHome) {
|
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
113
|
known[MARKETPLACE_NAME] = {
|
|
114
|
-
source: { source: '
|
|
114
|
+
source: { source: 'directory', path: root },
|
|
115
115
|
installLocation: root,
|
|
116
116
|
lastUpdated: new Date().toISOString(),
|
|
117
117
|
};
|
|
@@ -88,6 +88,43 @@ export const PRESETS = [
|
|
|
88
88
|
ANTHROPIC_SMALL_FAST_MODEL: 'deepseek/deepseek-chat-v3-0324',
|
|
89
89
|
},
|
|
90
90
|
},
|
|
91
|
+
// ----- xAI Grok Build CLI (native host) -----
|
|
92
|
+
{
|
|
93
|
+
name: 'grok-fast',
|
|
94
|
+
description: 'xAI Grok Build CLI — fast tier. Native grok host, no OpenRouter wrapper.',
|
|
95
|
+
provider: 'xai',
|
|
96
|
+
host: 'grok',
|
|
97
|
+
authEnvVar: 'XAI_API_KEY',
|
|
98
|
+
signupUrl: 'https://console.x.ai',
|
|
99
|
+
env: {
|
|
100
|
+
// TODO: confirm model id (docs.x.ai/build/models, May 2026)
|
|
101
|
+
GROK_MODEL: 'grok-build-fast',
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: 'grok-heavy',
|
|
106
|
+
description: 'xAI Grok Build CLI — heavy tier (SuperGrok). Native grok host.',
|
|
107
|
+
provider: 'xai',
|
|
108
|
+
host: 'grok',
|
|
109
|
+
authEnvVar: 'XAI_API_KEY',
|
|
110
|
+
signupUrl: 'https://console.x.ai',
|
|
111
|
+
env: {
|
|
112
|
+
// TODO: confirm model id (docs.x.ai/build/models, May 2026)
|
|
113
|
+
GROK_MODEL: 'grok-build',
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
// ----- Google Antigravity CLI (native host) -----
|
|
117
|
+
{
|
|
118
|
+
name: 'agy',
|
|
119
|
+
description: 'Google Antigravity CLI default. Auth via Google OAuth or ANTIGRAVITY_API_KEY.',
|
|
120
|
+
provider: 'google',
|
|
121
|
+
host: 'antigravity',
|
|
122
|
+
authEnvVar: 'ANTIGRAVITY_API_KEY',
|
|
123
|
+
signupUrl: 'https://antigravity.google',
|
|
124
|
+
env: {
|
|
125
|
+
// TODO: confirm model id — antigravity defaults are managed by the CLI itself
|
|
126
|
+
},
|
|
127
|
+
},
|
|
91
128
|
];
|
|
92
129
|
/** Look up a preset by name (case-sensitive). */
|
|
93
130
|
export function getPreset(name) {
|
package/dist/lib/registry.d.ts
CHANGED
|
@@ -24,6 +24,24 @@ export declare function searchMcpRegistries(query: string, options?: {
|
|
|
24
24
|
registry?: string;
|
|
25
25
|
limit?: number;
|
|
26
26
|
}): Promise<RegistrySearchResult[]>;
|
|
27
|
+
/**
|
|
28
|
+
* Convert an MCP server registry entry into an install spec suitable for
|
|
29
|
+
* writing into `manifest.mcp`. Returns `null` if the entry has no package we
|
|
30
|
+
* know how to launch (e.g. only remote endpoints, which the current manifest
|
|
31
|
+
* shape supports via `url`+`transport: 'http'` but isn't yet wired to the
|
|
32
|
+
* registry's `remotes` field).
|
|
33
|
+
*
|
|
34
|
+
* Supported package shapes:
|
|
35
|
+
* - npm / runtime=node → `npx -y <name>`
|
|
36
|
+
* - pypi / runtime=python → `uvx <name>`
|
|
37
|
+
* - runtime=docker → `docker run --rm -i <name>`
|
|
38
|
+
* - runtime=binary → `<name>` (assumed to be on PATH)
|
|
39
|
+
*/
|
|
40
|
+
export declare function mcpEntryToInstallSpec(entry: McpServerEntry): {
|
|
41
|
+
command?: string;
|
|
42
|
+
url?: string;
|
|
43
|
+
transport: 'stdio' | 'http';
|
|
44
|
+
} | null;
|
|
27
45
|
/** Look up detailed info for an MCP server by exact name. */
|
|
28
46
|
export declare function getMcpServerInfo(serverName: string, registryName?: string): Promise<McpServerEntry | null>;
|
|
29
47
|
/** Search skill registries for entries matching a query string. */
|
package/dist/lib/registry.js
CHANGED
|
@@ -113,6 +113,50 @@ export async function searchMcpRegistries(query, options) {
|
|
|
113
113
|
}
|
|
114
114
|
return results;
|
|
115
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Convert an MCP server registry entry into an install spec suitable for
|
|
118
|
+
* writing into `manifest.mcp`. Returns `null` if the entry has no package we
|
|
119
|
+
* know how to launch (e.g. only remote endpoints, which the current manifest
|
|
120
|
+
* shape supports via `url`+`transport: 'http'` but isn't yet wired to the
|
|
121
|
+
* registry's `remotes` field).
|
|
122
|
+
*
|
|
123
|
+
* Supported package shapes:
|
|
124
|
+
* - npm / runtime=node → `npx -y <name>`
|
|
125
|
+
* - pypi / runtime=python → `uvx <name>`
|
|
126
|
+
* - runtime=docker → `docker run --rm -i <name>`
|
|
127
|
+
* - runtime=binary → `<name>` (assumed to be on PATH)
|
|
128
|
+
*/
|
|
129
|
+
export function mcpEntryToInstallSpec(entry) {
|
|
130
|
+
const pkg = entry.packages?.[0];
|
|
131
|
+
if (!pkg)
|
|
132
|
+
return null;
|
|
133
|
+
// Remote transports (sse / streamable-http) need a URL the registry doesn't
|
|
134
|
+
// currently expose in this client's type. Skip for now; caller can fall back
|
|
135
|
+
// to manual --transport http with an explicit URL.
|
|
136
|
+
if (pkg.transport === 'sse' || pkg.transport === 'streamable-http') {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
const reg = pkg.registry_name?.toLowerCase();
|
|
140
|
+
const runtime = pkg.runtime;
|
|
141
|
+
const name = pkg.name;
|
|
142
|
+
if (!name)
|
|
143
|
+
return null;
|
|
144
|
+
if (reg === 'npm' || runtime === 'node') {
|
|
145
|
+
return { command: `npx -y ${name}`, transport: 'stdio' };
|
|
146
|
+
}
|
|
147
|
+
if (reg === 'pypi' || runtime === 'python') {
|
|
148
|
+
return { command: `uvx ${name}`, transport: 'stdio' };
|
|
149
|
+
}
|
|
150
|
+
if (runtime === 'docker') {
|
|
151
|
+
return { command: `docker run --rm -i ${name}`, transport: 'stdio' };
|
|
152
|
+
}
|
|
153
|
+
if (runtime === 'binary') {
|
|
154
|
+
return { command: name, transport: 'stdio' };
|
|
155
|
+
}
|
|
156
|
+
// Unknown registry/runtime — fall back to bare name so the user gets *something*
|
|
157
|
+
// to inspect via `agents mcp view`, rather than a silent miss.
|
|
158
|
+
return { command: name, transport: 'stdio' };
|
|
159
|
+
}
|
|
116
160
|
/** Look up detailed info for an MCP server by exact name. */
|
|
117
161
|
export async function getMcpServerInfo(serverName, registryName) {
|
|
118
162
|
const registries = getEnabledRegistries('mcp');
|
|
@@ -15,7 +15,7 @@ import * as yaml from 'yaml';
|
|
|
15
15
|
import * as TOML from 'smol-toml';
|
|
16
16
|
import { getSystemMcpDir, getUserMcpDir, getProjectAgentsDir, getEnabledExtraRepos, } from '../state.js';
|
|
17
17
|
/** Agents from resources/types.ts that support MCP. */
|
|
18
|
-
const MCP_CAPABLE_AGENTS = ['claude', 'codex', 'gemini', 'cursor', 'opencode', 'openclaw'];
|
|
18
|
+
const MCP_CAPABLE_AGENTS = ['claude', 'codex', 'gemini', 'cursor', 'opencode', 'openclaw', 'antigravity', 'grok'];
|
|
19
19
|
/**
|
|
20
20
|
* Parse an MCP YAML file into an McpItem.
|
|
21
21
|
*/
|
|
@@ -119,6 +119,11 @@ export function getMcpConfigPath(agent, versionHome) {
|
|
|
119
119
|
return path.join(versionHome, '.gemini', 'settings.json');
|
|
120
120
|
case 'openclaw':
|
|
121
121
|
return path.join(versionHome, '.openclaw', 'openclaw.json');
|
|
122
|
+
case 'antigravity':
|
|
123
|
+
// agy nests under ~/.gemini/antigravity-cli/ (shared parent with Gemini, distinct subdir).
|
|
124
|
+
return path.join(versionHome, '.gemini', 'antigravity-cli', 'mcp_config.json');
|
|
125
|
+
case 'grok':
|
|
126
|
+
return path.join(versionHome, '.grok', 'mcp.json');
|
|
122
127
|
default:
|
|
123
128
|
return null;
|
|
124
129
|
}
|
|
@@ -234,6 +239,39 @@ function syncToCodexConfig(configPath, items) {
|
|
|
234
239
|
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
235
240
|
fs.writeFileSync(configPath, TOML.stringify(config), 'utf-8');
|
|
236
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* Write MCP servers to Grok config.toml format ([mcp_servers] section).
|
|
244
|
+
*/
|
|
245
|
+
function syncToGrokConfig(configPath, items) {
|
|
246
|
+
let config = {};
|
|
247
|
+
if (fs.existsSync(configPath)) {
|
|
248
|
+
try {
|
|
249
|
+
config = TOML.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
config = {};
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
const mcpServers = {};
|
|
256
|
+
for (const item of items) {
|
|
257
|
+
if (item.transport === 'stdio') {
|
|
258
|
+
mcpServers[item.name] = {
|
|
259
|
+
command: item.command,
|
|
260
|
+
args: item.args || [],
|
|
261
|
+
...(item.env && { env: item.env }),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
else if (item.transport === 'http' || item.transport === 'sse') {
|
|
265
|
+
mcpServers[item.name] = {
|
|
266
|
+
url: item.url,
|
|
267
|
+
...(item.headers && { headers: item.headers }),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
config.mcp_servers = mcpServers;
|
|
272
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
273
|
+
fs.writeFileSync(configPath, TOML.stringify(config), 'utf-8');
|
|
274
|
+
}
|
|
237
275
|
/**
|
|
238
276
|
* Write MCP servers to OpenCode opencode.jsonc format.
|
|
239
277
|
*/
|
|
@@ -462,11 +500,15 @@ export const McpHandler = {
|
|
|
462
500
|
case 'openclaw':
|
|
463
501
|
syncToOpenClawConfig(configPath, mcpItems);
|
|
464
502
|
break;
|
|
503
|
+
case 'grok':
|
|
504
|
+
syncToGrokConfig(configPath, mcpItems);
|
|
505
|
+
break;
|
|
465
506
|
}
|
|
466
507
|
},
|
|
467
508
|
format(agent) {
|
|
468
509
|
switch (agent) {
|
|
469
510
|
case 'codex':
|
|
511
|
+
case 'grok':
|
|
470
512
|
return 'toml';
|
|
471
513
|
default:
|
|
472
514
|
return 'json';
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* - Union: All resources from all layers are combined
|
|
6
6
|
* - Override on name conflict: Higher layer wins (project > user > system)
|
|
7
7
|
*/
|
|
8
|
-
export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode' | 'openclaw';
|
|
8
|
+
export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode' | 'openclaw' | 'antigravity' | 'grok';
|
|
9
9
|
export type Layer = 'system' | 'user' | 'project';
|
|
10
10
|
export type ResourceKind = 'command' | 'hook' | 'skill' | 'rule' | 'mcp' | 'permission' | 'subagent' | 'workflow';
|
|
11
11
|
/** A resolved resource with its origin layer. */
|
package/dist/lib/resources.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import type { AgentId } from './types.js';
|
|
6
6
|
import { type SkillParseError } from './skills.js';
|
|
7
7
|
/** Resource kind — matches the subdirectory name under each repo root. */
|
|
8
|
-
export type ResourceKind = 'commands' | 'skills' | 'hooks' | 'rules' | 'mcp' | 'permissions' | 'subagents' | 'profiles' | 'secrets';
|
|
8
|
+
export type ResourceKind = 'commands' | 'skills' | 'hooks' | 'rules' | 'mcp' | 'cli' | 'permissions' | 'subagents' | 'profiles' | 'secrets';
|
|
9
9
|
/** A resource resolved with its origin. */
|
|
10
10
|
export interface ResolvedResource {
|
|
11
11
|
name: string;
|
package/dist/lib/rotate.js
CHANGED
|
@@ -10,7 +10,7 @@ import * as yaml from 'yaml';
|
|
|
10
10
|
import { getAccountInfo } from './agents.js';
|
|
11
11
|
import { readMeta, writeMeta, getHelpersDir, getUserAgentsDir } from './state.js';
|
|
12
12
|
import { listInstalledVersions, getVersionHomePath, resolveVersion } from './versions.js';
|
|
13
|
-
import { getUsageInfoByIdentity, getUsageLookupKey,
|
|
13
|
+
import { getUsageInfoByIdentity, getUsageLookupKey, } from './usage.js';
|
|
14
14
|
function getRotateDir() {
|
|
15
15
|
const dir = path.join(getHelpersDir(), 'rotate');
|
|
16
16
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -233,9 +233,15 @@ async function collectRunCandidates(agent) {
|
|
|
233
233
|
const rows = await Promise.all(versions.map(async (version) => {
|
|
234
234
|
const home = getVersionHomePath(agent, version);
|
|
235
235
|
const info = await getAccountInfo(agent, home);
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
236
|
+
// `info.email` (from .claude.json's oauthAccount) is the auth heuristic.
|
|
237
|
+
// We used to additionally call isClaudeAuthValid(home), which reads
|
|
238
|
+
// "Claude Code-credentials-<hash>" from the system keychain. That item is
|
|
239
|
+
// written by Claude Code itself with its own process in the ACL, so our
|
|
240
|
+
// helper triggers a macOS keychain-authorization sheet on every probe —
|
|
241
|
+
// one per installed version, every time `agents run` cold-starts. If
|
|
242
|
+
// claude's stored token has actually expired, the spawned agent detects
|
|
243
|
+
// it at its own startup and re-auths; that's the correct UX.
|
|
244
|
+
const authValid = info.email != null;
|
|
239
245
|
return {
|
|
240
246
|
agent,
|
|
241
247
|
version,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure display helpers for `agents routines list`.
|
|
3
|
+
*
|
|
4
|
+
* No external dependencies. All functions are pure (no I/O, no side effects).
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Convert a cron expression to a human-readable phrase.
|
|
8
|
+
*
|
|
9
|
+
* Handles the common patterns. For anything unrecognized, returns the raw
|
|
10
|
+
* expression so the user still sees something useful. NEVER throws.
|
|
11
|
+
*/
|
|
12
|
+
export declare function humanizeCron(expr: string, _tz?: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* Convert a next-run Date into a human phrase relative to `now`.
|
|
15
|
+
*
|
|
16
|
+
* - null → '-'
|
|
17
|
+
* - same calendar day → 'today 9:00 AM'
|
|
18
|
+
* - next calendar day → 'tomorrow 9:00 AM'
|
|
19
|
+
* - within 7 days → 'Mon 9:00 AM'
|
|
20
|
+
* - further out → 'Jun 15, 9:00 AM'
|
|
21
|
+
*/
|
|
22
|
+
export declare function humanizeNextRun(date: Date | null, now: Date, tz?: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* Parse a repo string into a display label and an optional hyperlink target.
|
|
25
|
+
*
|
|
26
|
+
* Rules:
|
|
27
|
+
* - undefined / empty → display '-', href null
|
|
28
|
+
* - 'owner/name' (one slash) → display 'owner/name', href 'https://github.com/owner/name/pulls'
|
|
29
|
+
* - 'https://...' or 'http://…' → display hostname+path, href the URL verbatim
|
|
30
|
+
* - anything else → display raw string, href null
|
|
31
|
+
*/
|
|
32
|
+
export declare function formatRepoLink(repo: string | undefined): {
|
|
33
|
+
display: string;
|
|
34
|
+
href: string | null;
|
|
35
|
+
};
|