@phnx-labs/agents-cli 1.20.15 → 1.20.17

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.
Files changed (49) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/commands/secrets.js +53 -1
  3. package/dist/commands/sessions-sync.d.ts +13 -0
  4. package/dist/commands/sessions-sync.js +73 -0
  5. package/dist/commands/sessions.js +2 -0
  6. package/dist/commands/sync.d.ts +10 -3
  7. package/dist/commands/sync.js +72 -9
  8. package/dist/commands/view.js +11 -3
  9. package/dist/index.js +1 -1
  10. package/dist/lib/agents.d.ts +11 -0
  11. package/dist/lib/agents.js +11 -9
  12. package/dist/lib/daemon.d.ts +19 -0
  13. package/dist/lib/daemon.js +97 -2
  14. package/dist/lib/hooks.js +12 -0
  15. package/dist/lib/migrate.d.ts +22 -0
  16. package/dist/lib/migrate.js +99 -1
  17. package/dist/lib/plugin-marketplace.d.ts +15 -0
  18. package/dist/lib/plugin-marketplace.js +54 -0
  19. package/dist/lib/secrets/drivers/rush.d.ts +14 -0
  20. package/dist/lib/secrets/drivers/rush.js +84 -0
  21. package/dist/lib/secrets/index.js +20 -0
  22. package/dist/lib/secrets/linux.js +88 -10
  23. package/dist/lib/secrets/sync-backend.d.ts +48 -0
  24. package/dist/lib/secrets/sync-backend.js +13 -0
  25. package/dist/lib/secrets/sync.d.ts +15 -23
  26. package/dist/lib/secrets/sync.js +31 -66
  27. package/dist/lib/session/parse.d.ts +2 -0
  28. package/dist/lib/session/parse.js +168 -2
  29. package/dist/lib/session/sync/agents.d.ts +46 -0
  30. package/dist/lib/session/sync/agents.js +94 -0
  31. package/dist/lib/session/sync/config.d.ts +30 -0
  32. package/dist/lib/session/sync/config.js +58 -0
  33. package/dist/lib/session/sync/crdt.d.ts +44 -0
  34. package/dist/lib/session/sync/crdt.js +119 -0
  35. package/dist/lib/session/sync/manifest.d.ts +51 -0
  36. package/dist/lib/session/sync/manifest.js +96 -0
  37. package/dist/lib/session/sync/r2.d.ts +32 -0
  38. package/dist/lib/session/sync/r2.js +121 -0
  39. package/dist/lib/session/sync/sync.d.ts +82 -0
  40. package/dist/lib/session/sync/sync.js +251 -0
  41. package/dist/lib/shims.d.ts +1 -1
  42. package/dist/lib/shims.js +17 -1
  43. package/dist/lib/sync-umbrella.d.ts +76 -0
  44. package/dist/lib/sync-umbrella.js +125 -0
  45. package/dist/lib/teams/parsers.js +159 -1
  46. package/dist/lib/usage.d.ts +18 -0
  47. package/dist/lib/usage.js +25 -0
  48. package/dist/lib/versions.js +30 -13
  49. package/package.json +2 -1
@@ -2,7 +2,7 @@
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, Grok, Antigravity) into a unified
5
+ * (Claude, Codex, Gemini, Cursor, OpenCode, Grok, Antigravity, Kimi) into a unified
6
6
  * event schema with consistent types: init, message, tool_use, bash,
7
7
  * file_read, file_write, file_create, file_delete, result, error, and others.
8
8
  */
@@ -31,6 +31,9 @@ export function normalizeEvents(agentType, raw) {
31
31
  else if (agentType === 'antigravity') {
32
32
  return normalizeAntigravity(raw);
33
33
  }
34
+ else if (agentType === 'kimi') {
35
+ return normalizeKimi(raw);
36
+ }
34
37
  // droid (Factory AI) intentionally falls through to the generic normalizer
35
38
  // below: its `-o stream-json` JSONL event schema is not yet verified against
36
39
  // a live run (the documented `debug` format differs from stream-json). Events
@@ -908,6 +911,161 @@ function normalizeGrok(raw) {
908
911
  timestamp: timestamp,
909
912
  }];
910
913
  }
914
+ // --- Kimi parsing ---
915
+ // Kimi's `--output-format stream-json` emits one JSON object per line with a
916
+ // simple `role`-based schema:
917
+ // - {"role":"assistant","content":"..."} → final message
918
+ // - {"role":"assistant","tool_calls":[{"function":{"name":"Bash","arguments":"<json>"}}]} → tool use
919
+ // - {"role":"tool","tool_call_id":"...","content":"..."} → tool result
920
+ // - {"role":"meta","type":"session.resume_hint","session_id":"..."} → init / session id
921
+ // Tool arguments are JSON-stringified inside `function.arguments` and must be
922
+ // parsed before extracting paths/commands. Verified against live `kimi` runs.
923
+ function normalizeKimi(raw) {
924
+ const timestamp = new Date().toISOString();
925
+ if (!raw || typeof raw !== 'object') {
926
+ return [{
927
+ type: 'unknown',
928
+ agent: 'kimi',
929
+ raw: raw,
930
+ timestamp: timestamp,
931
+ }];
932
+ }
933
+ const role = typeof raw.role === 'string' ? raw.role : '';
934
+ // Assistant message (final answer or tool-call request).
935
+ if (role === 'assistant') {
936
+ const events = [];
937
+ if (typeof raw.content === 'string' && raw.content) {
938
+ events.push({
939
+ type: 'message',
940
+ agent: 'kimi',
941
+ content: raw.content,
942
+ complete: true,
943
+ timestamp: timestamp,
944
+ });
945
+ }
946
+ const toolCalls = Array.isArray(raw.tool_calls) ? raw.tool_calls : [];
947
+ for (const toolCall of toolCalls) {
948
+ const fn = toolCall?.function || {};
949
+ const toolName = typeof fn.name === 'string' ? fn.name : 'unknown';
950
+ let toolArgs = {};
951
+ if (typeof fn.arguments === 'string') {
952
+ try {
953
+ toolArgs = JSON.parse(fn.arguments);
954
+ }
955
+ catch {
956
+ toolArgs = { _raw: fn.arguments };
957
+ }
958
+ }
959
+ else if (fn.arguments && typeof fn.arguments === 'object') {
960
+ toolArgs = fn.arguments;
961
+ }
962
+ const filePath = toolArgs?.path || toolArgs?.file_path || '';
963
+ const command = toolArgs?.command || '';
964
+ // Map known tools to structured events. If a known tool is missing the
965
+ // fields we need (e.g. unparseable arguments), fall back to tool_use so
966
+ // the event is still visible in summaries rather than dropped.
967
+ let normalized = null;
968
+ if (toolName === 'Bash' && command) {
969
+ const bashEvents = [{
970
+ type: 'bash',
971
+ agent: 'kimi',
972
+ tool: toolName,
973
+ command: command,
974
+ timestamp: timestamp,
975
+ }];
976
+ const [filesRead, filesWritten, filesDeleted] = extractFileOpsFromBash(command);
977
+ for (const p of filesRead) {
978
+ bashEvents.push({ type: 'file_read', agent: 'kimi', tool: 'bash', path: p, command, timestamp });
979
+ }
980
+ for (const p of filesWritten) {
981
+ bashEvents.push({ type: 'file_write', agent: 'kimi', tool: 'bash', path: p, command, timestamp });
982
+ }
983
+ for (const p of filesDeleted) {
984
+ bashEvents.push({ type: 'file_delete', agent: 'kimi', tool: 'bash', path: p, command, timestamp });
985
+ }
986
+ normalized = bashEvents;
987
+ }
988
+ else if (toolName === 'Read' && filePath) {
989
+ normalized = [{
990
+ type: 'file_read',
991
+ agent: 'kimi',
992
+ tool: toolName,
993
+ path: filePath,
994
+ timestamp: timestamp,
995
+ }];
996
+ }
997
+ else if (toolName === 'Edit' && filePath) {
998
+ normalized = [{
999
+ type: 'file_write',
1000
+ agent: 'kimi',
1001
+ tool: toolName,
1002
+ path: filePath,
1003
+ timestamp: timestamp,
1004
+ }];
1005
+ }
1006
+ else if (toolName === 'Write' && filePath) {
1007
+ normalized = [{
1008
+ type: 'file_create',
1009
+ agent: 'kimi',
1010
+ tool: toolName,
1011
+ path: filePath,
1012
+ timestamp: timestamp,
1013
+ }];
1014
+ }
1015
+ if (normalized) {
1016
+ events.push(...normalized);
1017
+ }
1018
+ else {
1019
+ events.push({
1020
+ type: 'tool_use',
1021
+ agent: 'kimi',
1022
+ tool: toolName,
1023
+ args: toolArgs,
1024
+ timestamp: timestamp,
1025
+ });
1026
+ }
1027
+ }
1028
+ return events.length > 0 ? events : [];
1029
+ }
1030
+ // Tool result (response to an assistant tool_call).
1031
+ if (role === 'tool') {
1032
+ const content = typeof raw.content === 'string' ? raw.content : '';
1033
+ const success = raw.isError !== true && !(content && content.startsWith('Error:'));
1034
+ return [{
1035
+ type: 'tool_result',
1036
+ agent: 'kimi',
1037
+ tool_call_id: typeof raw.tool_call_id === 'string' ? raw.tool_call_id : null,
1038
+ success: success,
1039
+ content: content,
1040
+ timestamp: timestamp,
1041
+ }];
1042
+ }
1043
+ // Meta events (session lifecycle).
1044
+ if (role === 'meta') {
1045
+ const metaType = typeof raw.type === 'string' ? raw.type : '';
1046
+ if (metaType === 'session.resume_hint') {
1047
+ return [{
1048
+ type: 'init',
1049
+ agent: 'kimi',
1050
+ session_id: typeof raw.session_id === 'string' ? raw.session_id : null,
1051
+ timestamp: timestamp,
1052
+ }];
1053
+ }
1054
+ return [{
1055
+ type: 'meta',
1056
+ agent: 'kimi',
1057
+ meta_type: metaType,
1058
+ raw: raw,
1059
+ timestamp: timestamp,
1060
+ }];
1061
+ }
1062
+ return [{
1063
+ type: raw.type || 'unknown',
1064
+ agent: 'kimi',
1065
+ raw: raw,
1066
+ timestamp: timestamp,
1067
+ }];
1068
+ }
911
1069
  // --- Antigravity parsing ---
912
1070
  // Intentionally conservative. Antigravity's `agy` binary advertises an
913
1071
  // `--output-format json` flag in its docs, but the released binary errors with
@@ -85,6 +85,24 @@ export declare function getUsageInfoByIdentity(inputs: UsageIdentityInput[]): Pr
85
85
  export declare function getUsageInfoForIdentity(input: UsageIdentityInput): Promise<UsageInfo>;
86
86
  /** Format a one-line usage summary with compact bars for inline display. */
87
87
  export declare function formatUsageSummary(plan: string | null, snapshot: UsageSnapshot | null, planWidth?: number): string;
88
+ /**
89
+ * Derive an account's real throttle state from its live usage windows — the
90
+ * same signal `agents usage` shows and balanced rotation trusts
91
+ * (`getRoutingUsedPercent` in rotate.ts). A window at 100% utilization means
92
+ * the account is throttled until that window resets.
93
+ *
94
+ * Returns `null` when there is no snapshot, so callers render no badge rather
95
+ * than a misleading one. This deliberately never consults
96
+ * `cachedExtraUsageDisabledReason`: that field describes why pay-as-you-go
97
+ * overage is disabled (`out_of_credits` = no overage credits purchased,
98
+ * `org_level_disabled` = an admin turned overage off), NOT whether the account
99
+ * can do work right now. A Pro account at 5% weekly usage with overage disabled
100
+ * is fully usable, yet that flag would mislabel it "out of credits".
101
+ *
102
+ * The model-specific `sonnet_week` sub-limit is excluded: hitting it throttles
103
+ * one model, not the account, so it shouldn't flip the whole row to throttled.
104
+ */
105
+ export declare function deriveUsageStatusFromSnapshot(snapshot: UsageSnapshot | null | undefined): 'available' | 'rate_limited' | null;
88
106
  /**
89
107
  * Compact colored badge for the account's overall usage status. Renders only
90
108
  * when the account is throttled — `available` and `null` return ''.
package/dist/lib/usage.js CHANGED
@@ -201,6 +201,31 @@ export function formatUsageSummary(plan, snapshot, planWidth = 3) {
201
201
  }
202
202
  return parts.join(' ');
203
203
  }
204
+ /**
205
+ * Derive an account's real throttle state from its live usage windows — the
206
+ * same signal `agents usage` shows and balanced rotation trusts
207
+ * (`getRoutingUsedPercent` in rotate.ts). A window at 100% utilization means
208
+ * the account is throttled until that window resets.
209
+ *
210
+ * Returns `null` when there is no snapshot, so callers render no badge rather
211
+ * than a misleading one. This deliberately never consults
212
+ * `cachedExtraUsageDisabledReason`: that field describes why pay-as-you-go
213
+ * overage is disabled (`out_of_credits` = no overage credits purchased,
214
+ * `org_level_disabled` = an admin turned overage off), NOT whether the account
215
+ * can do work right now. A Pro account at 5% weekly usage with overage disabled
216
+ * is fully usable, yet that flag would mislabel it "out of credits".
217
+ *
218
+ * The model-specific `sonnet_week` sub-limit is excluded: hitting it throttles
219
+ * one model, not the account, so it shouldn't flip the whole row to throttled.
220
+ */
221
+ export function deriveUsageStatusFromSnapshot(snapshot) {
222
+ if (!snapshot || snapshot.windows.length === 0)
223
+ return null;
224
+ const blocking = snapshot.windows.filter((window) => window.key !== 'sonnet_week');
225
+ const windows = blocking.length > 0 ? blocking : snapshot.windows;
226
+ const maxUsed = Math.max(...windows.map((window) => window.usedPercent));
227
+ return maxUsed >= 100 ? 'rate_limited' : 'available';
228
+ }
204
229
  /**
205
230
  * Compact colored badge for the account's overall usage status. Renders only
206
231
  * when the account is throttled — `available` and `null` return ''.
@@ -25,7 +25,7 @@ import { checkbox, select } from '@inquirer/prompts';
25
25
  import { getVersionsDir, ensureAgentsDir, readMeta, writeMeta, getCommandsDir, getSkillsDir, getHooksDir, getResolvedRulesDir, getUserRulesDir, getVersionResources, ensureVersionResourcePatterns, getProjectAgentsDir, getPromptcutsPath, getUserPromptcutsPath, getEnabledExtraRepos, getAgentsDir, getUserAgentsDir, getTrashVersionsDir, getActiveRulesPreset } from './state.js';
26
26
  import { defaultPatterns, expandPatterns } from './resource-patterns.js';
27
27
  import { listResources } from './resources.js';
28
- import { AGENTS, agentConfigDirName, getAccountEmail, resolveAgentName, formatAgentError } from './agents.js';
28
+ import { AGENTS, agentConfigDirName, getAccountEmail, resolveAgentName, formatAgentError, findInPath } from './agents.js';
29
29
  import { discoverPermissionGroups, getActivePermissionPresetName, readPermissionPresetRecipe, PERMISSION_PRESET_ENV_VAR } from './permissions.js';
30
30
  import { parseMcpServerConfig } from './mcp.js';
31
31
  import { createVersionedAlias, removeVersionedAlias, getConfigSymlinkVersion, ensureClaudeInsideSymlink } from './shims.js';
@@ -818,6 +818,14 @@ export function getBinaryPath(agent, version) {
818
818
  catch { }
819
819
  return path.join(grokDownloads, `grok-${version}`);
820
820
  }
821
+ if (agent === 'droid') {
822
+ // Factory's installer drops a standalone native binary at ~/.local/bin/droid
823
+ // (no npm package, nothing in node_modules/.bin). The binary is global, not
824
+ // per-version — config isolation rides the ~/.factory symlink switch, not a
825
+ // separate binary per version. Mirror the shim's `droid` branch so
826
+ // isVersionInstalled/`agents view` agree with what actually executes.
827
+ return path.join(os.homedir(), '.local', 'bin', 'droid');
828
+ }
821
829
  const versionDir = getVersionDir(agent, version);
822
830
  return path.join(versionDir, 'node_modules', '.bin', agentConfig.cliCommand);
823
831
  }
@@ -1005,19 +1013,28 @@ export async function installVersion(agent, version, onProgress) {
1005
1013
  // but `agents view` shows the agent under "Not Managed" because
1006
1014
  // listInstalledVersions returns [] — the installer drops the binary in
1007
1015
  // ~/.local/bin (or similar) rather than the version's node_modules/.bin.
1008
- // Grok is special-cased in getBinaryPath itself (binary lives in
1009
- // ~/.grok/downloads), so we skip the symlink there.
1010
- if (agent !== 'grok') {
1011
- try {
1012
- const { stdout: whichOut } = await execFileAsync(process.platform === 'win32' ? 'where' : 'which', [agentConfig.cliCommand]);
1013
- const installedBinary = whichOut.trim().split('\n')[0];
1014
- if (installedBinary && fs.existsSync(installedBinary)) {
1015
- importInstallScriptBinary({ agentId: agent, npmPackage: agentConfig.npmPackage, cliCommand: agentConfig.cliCommand }, installedVersion, installedBinary, versionDir);
1016
- }
1017
- }
1018
- catch {
1019
- /* binary missing from PATH install script failed silently; surface via the existing version.install error path below isn't possible here since the script returned 0. Leave the version dir empty so getBinaryPath check correctly reports it uninstalled. */
1016
+ //
1017
+ // Agents whose binary is special-cased in getBinaryPath (grok ->
1018
+ // ~/.grok/downloads, droid -> ~/.local/bin/droid) need no symlink — and
1019
+ // creating one is actively harmful: `which <cli>` can resolve to OUR OWN
1020
+ // dispatcher shim, because ~/.agents/.cache/shims sits ahead of ~/.local/bin
1021
+ // on PATH. Symlinking node_modules/.bin/<cli> at the shim makes the shim
1022
+ // exec itself forever. So we skip the resolver-backed agents here AND, for
1023
+ // everyone else, filter the shims dir out of the `which` candidates so the
1024
+ // same race can't bite a non-special-cased installScript agent.
1025
+ if (agent !== 'grok' && agent !== 'droid') {
1026
+ // findInPath is a pure-Node PATH scan that already skips our own shims
1027
+ // dirso it returns the genuine install, never our dispatcher shim
1028
+ // (which sits ahead of ~/.local/bin on PATH and would otherwise be
1029
+ // captured, producing a self-referential node_modules/.bin/<cli> link
1030
+ // that exec-loops forever).
1031
+ const installedBinary = findInPath(agentConfig.cliCommand);
1032
+ if (installedBinary) {
1033
+ importInstallScriptBinary({ agentId: agent, npmPackage: agentConfig.npmPackage, cliCommand: agentConfig.cliCommand }, installedVersion, installedBinary, versionDir);
1020
1034
  }
1035
+ /* If null: binary missing from PATH (install script failed silently) or
1036
+ only our shim is present. Leave the version dir empty so getBinaryPath
1037
+ correctly reports it uninstalled. */
1021
1038
  }
1022
1039
  createVersionedAlias(agent, installedVersion);
1023
1040
  emit('version.install', { agent, version: installedVersion });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phnx-labs/agents-cli",
3
- "version": "1.20.15",
3
+ "version": "1.20.17",
4
4
  "description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -82,6 +82,7 @@
82
82
  "@types/proper-lockfile": "4.1.4",
83
83
  "@xterm/headless": "6.0.0",
84
84
  "@zed-industries/agent-client-protocol": "0.4.5",
85
+ "aws4fetch": "1.0.20",
85
86
  "chalk": "5.6.2",
86
87
  "commander": "15.0.0",
87
88
  "croner": "10.0.1",