@phnx-labs/agents-cli 1.22.22 → 1.22.24

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 (138) hide show
  1. package/CHANGELOG.md +402 -0
  2. package/README.md +13 -6
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/attach.d.ts +2 -0
  5. package/dist/commands/attach.js +32 -7
  6. package/dist/commands/defaults.js +2 -0
  7. package/dist/commands/doctor.js +20 -7
  8. package/dist/commands/exec.js +100 -34
  9. package/dist/commands/feed.d.ts +18 -0
  10. package/dist/commands/feed.js +44 -1
  11. package/dist/commands/focus.d.ts +108 -5
  12. package/dist/commands/focus.js +455 -32
  13. package/dist/commands/fork.d.ts +20 -2
  14. package/dist/commands/fork.js +91 -64
  15. package/dist/commands/go.d.ts +39 -1
  16. package/dist/commands/go.js +112 -7
  17. package/dist/commands/harness-wizard.d.ts +206 -0
  18. package/dist/commands/harness-wizard.js +403 -0
  19. package/dist/commands/harness.d.ts +12 -0
  20. package/dist/commands/harness.js +97 -101
  21. package/dist/commands/insights.js +6 -2
  22. package/dist/commands/inspect.js +39 -5
  23. package/dist/commands/menubar.js +6 -1
  24. package/dist/commands/models.js +1 -0
  25. package/dist/commands/modes.d.ts +12 -0
  26. package/dist/commands/modes.js +147 -0
  27. package/dist/commands/resume.js +10 -3
  28. package/dist/commands/secrets-sync.js +11 -13
  29. package/dist/commands/secrets.d.ts +2 -0
  30. package/dist/commands/secrets.js +79 -47
  31. package/dist/commands/sessions-browser.d.ts +35 -0
  32. package/dist/commands/sessions-browser.js +140 -14
  33. package/dist/commands/sessions-resume.d.ts +23 -3
  34. package/dist/commands/sessions-resume.js +73 -19
  35. package/dist/commands/sessions.d.ts +91 -5
  36. package/dist/commands/sessions.js +359 -125
  37. package/dist/commands/setup-secrets.js +1 -1
  38. package/dist/commands/sync.js +246 -42
  39. package/dist/commands/view.js +2 -0
  40. package/dist/commands/watchdog.js +13 -2
  41. package/dist/index.js +2 -1
  42. package/dist/lib/agent-modes.d.ts +49 -0
  43. package/dist/lib/agent-modes.js +70 -0
  44. package/dist/lib/agents.d.ts +1 -1
  45. package/dist/lib/agents.js +132 -0
  46. package/dist/lib/codex-policy.d.ts +17 -0
  47. package/dist/lib/codex-policy.js +48 -0
  48. package/dist/lib/crabbox/lease.d.ts +25 -0
  49. package/dist/lib/crabbox/lease.js +62 -0
  50. package/dist/lib/daemon.js +70 -0
  51. package/dist/lib/doctor-diff.d.ts +3 -0
  52. package/dist/lib/doctor-diff.js +15 -13
  53. package/dist/lib/event-stream.d.ts +3 -1
  54. package/dist/lib/event-stream.js +14 -1
  55. package/dist/lib/exec.d.ts +16 -0
  56. package/dist/lib/exec.js +175 -62
  57. package/dist/lib/feed-broadcast.d.ts +1 -20
  58. package/dist/lib/feed-broadcast.js +31 -1
  59. package/dist/lib/hooks/cache.js +36 -3
  60. package/dist/lib/hooks.d.ts +19 -7
  61. package/dist/lib/hooks.js +100 -40
  62. package/dist/lib/hosts/session-index.d.ts +4 -0
  63. package/dist/lib/hosts/session-index.js +7 -0
  64. package/dist/lib/manifest.d.ts +12 -2
  65. package/dist/lib/manifest.js +60 -5
  66. package/dist/lib/mcp.js +44 -0
  67. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  68. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  69. package/dist/lib/menubar/install-menubar.d.ts +52 -2
  70. package/dist/lib/menubar/install-menubar.js +128 -6
  71. package/dist/lib/models.d.ts +0 -5
  72. package/dist/lib/models.js +48 -0
  73. package/dist/lib/plugin-marketplace.js +9 -0
  74. package/dist/lib/pricing/prices.json +119 -92
  75. package/dist/lib/pricing/table.js +13 -0
  76. package/dist/lib/refresh.d.ts +5 -0
  77. package/dist/lib/refresh.js +37 -33
  78. package/dist/lib/remote-agents-json.d.ts +29 -1
  79. package/dist/lib/remote-agents-json.js +47 -10
  80. package/dist/lib/resource-inventory.d.ts +79 -0
  81. package/dist/lib/resource-inventory.js +122 -0
  82. package/dist/lib/resources/mcp.js +2 -0
  83. package/dist/lib/resources/permissions.js +3 -0
  84. package/dist/lib/resources/types.d.ts +2 -1
  85. package/dist/lib/resources.js +8 -5
  86. package/dist/lib/run-defaults.d.ts +2 -0
  87. package/dist/lib/run-defaults.js +23 -2
  88. package/dist/lib/runner.js +50 -36
  89. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  90. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  91. package/dist/lib/secrets/bundles.js +157 -65
  92. package/dist/lib/secrets/filestore.d.ts +5 -3
  93. package/dist/lib/secrets/filestore.js +12 -8
  94. package/dist/lib/secrets/index.d.ts +15 -1
  95. package/dist/lib/secrets/index.js +148 -53
  96. package/dist/lib/secrets/reaper.d.ts +97 -0
  97. package/dist/lib/secrets/reaper.js +219 -0
  98. package/dist/lib/secrets/remote.d.ts +29 -0
  99. package/dist/lib/secrets/remote.js +37 -1
  100. package/dist/lib/secrets/sync-passphrase.d.ts +27 -0
  101. package/dist/lib/secrets/sync-passphrase.js +78 -0
  102. package/dist/lib/session/active.d.ts +36 -1
  103. package/dist/lib/session/active.js +60 -19
  104. package/dist/lib/session/actor-sidecar.d.ts +14 -0
  105. package/dist/lib/session/actor-sidecar.js +67 -4
  106. package/dist/lib/session/db.d.ts +1 -1
  107. package/dist/lib/session/db.js +32 -1
  108. package/dist/lib/session/discover.js +168 -0
  109. package/dist/lib/session/parse.d.ts +10 -0
  110. package/dist/lib/session/parse.js +98 -0
  111. package/dist/lib/session/recovery.d.ts +37 -0
  112. package/dist/lib/session/recovery.js +95 -0
  113. package/dist/lib/session/remote-list.d.ts +10 -1
  114. package/dist/lib/session/remote-list.js +2 -8
  115. package/dist/lib/session/remote.d.ts +57 -3
  116. package/dist/lib/session/remote.js +90 -26
  117. package/dist/lib/session/resume-command.d.ts +6 -0
  118. package/dist/lib/session/resume-command.js +8 -0
  119. package/dist/lib/session/session-cache.d.ts +173 -0
  120. package/dist/lib/session/session-cache.js +399 -0
  121. package/dist/lib/session/types.d.ts +1 -1
  122. package/dist/lib/session/types.js +1 -1
  123. package/dist/lib/session/width.d.ts +1 -1
  124. package/dist/lib/session/width.js +12 -2
  125. package/dist/lib/shims.d.ts +2 -2
  126. package/dist/lib/shims.js +62 -8
  127. package/dist/lib/staleness/writers/hooks.js +1 -1
  128. package/dist/lib/startup/command-registry.d.ts +1 -0
  129. package/dist/lib/startup/command-registry.js +2 -0
  130. package/dist/lib/sync-umbrella.d.ts +5 -0
  131. package/dist/lib/sync-umbrella.js +5 -4
  132. package/dist/lib/tmux/session.d.ts +4 -2
  133. package/dist/lib/tmux/session.js +5 -5
  134. package/dist/lib/types.d.ts +3 -1
  135. package/dist/lib/usage.d.ts +13 -0
  136. package/dist/lib/usage.js +215 -0
  137. package/dist/lib/versions.js +13 -2
  138. package/package.json +1 -1
@@ -18,6 +18,7 @@
18
18
  import { pullRepo } from './git.js';
19
19
  import { getUserAgentsDir, getEnabledExtraRepos } from './state.js';
20
20
  import { listRemoteBundles, pullBundle } from './secrets/sync.js';
21
+ import { SYNC_PASSPHRASE_ENV } from './secrets/sync-passphrase.js';
21
22
  /**
22
23
  * Decide which stages run. Pure — no I/O. Semantics:
23
24
  * bare (no flags) fetch repos, then reconcile
@@ -52,7 +53,7 @@ export function planUmbrellaStages(f) {
52
53
  * reconcile — `agents sync` should make as much current as it can in one pass.
53
54
  */
54
55
  export async function runUmbrellaSync(args) {
55
- const { flags, log, yes, passphrase } = args;
56
+ const { flags, log, yes, passphrase, quiet = false } = args;
56
57
  const plan = planUmbrellaStages(flags);
57
58
  const result = { plan, reconciled: false };
58
59
  if (plan.fetchRepos) {
@@ -79,10 +80,10 @@ export async function runUmbrellaSync(args) {
79
80
  result.secrets = {
80
81
  pulled: 0,
81
82
  skipped: true,
82
- reason: 'no passphrase — set AGENTS_SECRETS_PASSPHRASE or run `agents login` (#366)',
83
+ reason: `no passphrase — set ${SYNC_PASSPHRASE_ENV} or run \`agents login\` (#366)`,
83
84
  errors: [],
84
85
  };
85
- log('secrets: skipped (no passphrase available)');
86
+ log(`secrets: skipped (no passphrase — set ${SYNC_PASSPHRASE_ENV})`);
86
87
  }
87
88
  else {
88
89
  let pulled = 0;
@@ -108,7 +109,7 @@ export async function runUmbrellaSync(args) {
108
109
  }
109
110
  if (plan.reconcile) {
110
111
  const { refresh } = await import('./refresh.js');
111
- await refresh({ skipPrompts: yes });
112
+ await refresh({ skipPrompts: yes, quiet });
112
113
  result.reconciled = true;
113
114
  // Keep already-registered devices' reachability current, and surface newly
114
115
  // appeared tailnet nodes as "pending" for the menu-bar Register/Ignore gate
@@ -114,6 +114,8 @@ export interface TmuxClient {
114
114
  export declare function listClients(socket?: string): Promise<TmuxClient[]>;
115
115
  /** A dead pane's exit status, read from tmux while the pane lingers under remain-on-exit. */
116
116
  export interface PaneExit {
117
+ /** False when the pane/socket no longer exists or tmux could not query it. */
118
+ found: boolean;
117
119
  /** True once the process that ran in the pane has exited (pane is dead). */
118
120
  dead: boolean;
119
121
  /** Exit status of the dead pane's process, when tmux reports it. */
@@ -122,8 +124,8 @@ export interface PaneExit {
122
124
  /**
123
125
  * Read whether a pane's process has exited and, if so, its exit status. Used by
124
126
  * the spawn-wrap path to recover the wrapped agent's exit code after the attach
125
- * client returns. Returns `{ dead: false }` when tmux can't answer (session gone,
126
- * pane missing) so the caller treats an unreadable pane as "still alive / detach".
127
+ * client returns. `found: false` distinguishes a missing pane from a living one;
128
+ * focus must recover the former instead of attaching a retained/stale target.
127
129
  */
128
130
  export declare function paneExitStatus(pane: string, socket?: string): Promise<PaneExit>;
129
131
  /**
@@ -248,8 +248,8 @@ export async function listClients(socket) {
248
248
  /**
249
249
  * Read whether a pane's process has exited and, if so, its exit status. Used by
250
250
  * the spawn-wrap path to recover the wrapped agent's exit code after the attach
251
- * client returns. Returns `{ dead: false }` when tmux can't answer (session gone,
252
- * pane missing) so the caller treats an unreadable pane as "still alive / detach".
251
+ * client returns. `found: false` distinguishes a missing pane from a living one;
252
+ * focus must recover the former instead of attaching a retained/stale target.
253
253
  */
254
254
  export async function paneExitStatus(pane, socket) {
255
255
  let res;
@@ -261,13 +261,13 @@ export async function paneExitStatus(pane, socket) {
261
261
  });
262
262
  }
263
263
  catch {
264
- return { dead: false };
264
+ return { found: false, dead: false };
265
265
  }
266
266
  if (res.code !== 0)
267
- return { dead: false };
267
+ return { found: false, dead: false };
268
268
  const [deadRaw, statusRaw] = res.stdout.trim().split(/\s+/);
269
269
  const status = statusRaw !== undefined && statusRaw !== '' ? parseInt(statusRaw, 10) : undefined;
270
- return { dead: deadRaw === '1', status: Number.isFinite(status) ? status : undefined };
270
+ return { found: true, dead: deadRaw === '1', status: Number.isFinite(status) ? status : undefined };
271
271
  }
272
272
  /**
273
273
  * Bind a per-session hook. Used by the spawn-wrap path to install a `pane-died`
@@ -8,9 +8,10 @@
8
8
  import type { CloudProviderId } from './cloud/types.js';
9
9
  import type { FeedBroadcastConfig } from './feed-broadcast.js';
10
10
  /** Unique identifier for a current or legacy AI coding agent. */
11
- export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode' | 'openclaw' | 'copilot' | 'amp' | 'kiro' | 'goose' | 'antigravity' | 'grok' | 'kimi' | 'droid' | 'hermes' | 'pi';
11
+ export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode' | 'openclaw' | 'copilot' | 'amp' | 'kiro' | 'goose' | 'antigravity' | 'grok' | 'kimi' | 'droid' | 'hermes' | 'pi' | 'muse';
12
12
  /** How `agents run <agent>` chooses an installed version when none is pinned. */
13
13
  export type RunStrategy = 'pinned' | 'available' | 'balanced';
14
+ export type RunEffort = 'low' | 'medium' | 'high' | 'xhigh' | 'max' | 'auto';
14
15
  /**
15
16
  * Reserved `<agent>` keyword for `agents run auto` — full-auto dispatch:
16
17
  * host (14d launch affinity) → harness (best-account headroom, weighted) →
@@ -36,6 +37,7 @@ export interface AgentRunConfig {
36
37
  export interface RunDefaults {
37
38
  mode?: Mode;
38
39
  model?: string;
40
+ effort?: RunEffort;
39
41
  }
40
42
  /** `run:` section in agents.yaml. Agent keys keep strategy; `defaults` stores selector rules. */
41
43
  export type RunConfig = Partial<Record<AgentId, AgentRunConfig>> & {
@@ -141,6 +141,10 @@ declare const USAGE_SOURCES: {
141
141
  readonly fetch: typeof getAntigravityUsageInfo;
142
142
  readonly network: true;
143
143
  };
144
+ readonly muse: {
145
+ readonly fetch: typeof getMuseUsageInfo;
146
+ readonly network: true;
147
+ };
144
148
  };
145
149
  export declare const USAGE_SOURCE_AGENT_IDS: (keyof typeof USAGE_SOURCES)[];
146
150
  /** Fetch usage info for a given agent through the canonical source registry. */
@@ -510,6 +514,15 @@ export declare function renderBar(usedPercent: number, length: number, minimumVi
510
514
  export declare function getUsageColor(usedPercent: number): (text: string) => string;
511
515
  /** Parse the latest billing info from Grok's unified log. */
512
516
  declare function getGrokUsageInfo(options?: UsageOptions): Promise<UsageInfo>;
517
+ /**
518
+ * Muse Code usage.
519
+ *
520
+ * Prefer live Meta Model API rate-limit headers when a key is available
521
+ * (META_API_KEY / MODEL_API_KEY / ~/.config/muse/auth.json). Fall back to
522
+ * aggregating `model_completed.usage` from local session.jsonl logs under
523
+ * ~/.local/share/muse/sessions for a last-7-days token window.
524
+ */
525
+ declare function getMuseUsageInfo(options?: UsageOptions): Promise<UsageInfo>;
513
526
  /** Per-model bucket in Cursor's /api/usage response. */
514
527
  interface CursorUsageModel {
515
528
  numRequests?: number | null;
package/dist/lib/usage.js CHANGED
@@ -139,6 +139,7 @@ const USAGE_SOURCES = {
139
139
  grok: { fetch: getGrokUsageInfo, network: false },
140
140
  cursor: { fetch: getCursorUsageInfo, network: true },
141
141
  antigravity: { fetch: getAntigravityUsageInfo, network: true },
142
+ muse: { fetch: getMuseUsageInfo, network: true },
142
143
  };
143
144
  export const USAGE_SOURCE_AGENT_IDS = Object.keys(USAGE_SOURCES);
144
145
  function getUsageSource(agentId) {
@@ -1873,6 +1874,220 @@ async function getGrokUsageInfo(options) {
1873
1874
  return { snapshot: null, error: null };
1874
1875
  }
1875
1876
  }
1877
+ /**
1878
+ * Muse Code usage.
1879
+ *
1880
+ * Prefer live Meta Model API rate-limit headers when a key is available
1881
+ * (META_API_KEY / MODEL_API_KEY / ~/.config/muse/auth.json). Fall back to
1882
+ * aggregating `model_completed.usage` from local session.jsonl logs under
1883
+ * ~/.local/share/muse/sessions for a last-7-days token window.
1884
+ */
1885
+ async function getMuseUsageInfo(options) {
1886
+ try {
1887
+ const base = options?.home || os.homedir();
1888
+ const live = await probeMuseRateLimits(base);
1889
+ if (live)
1890
+ return { snapshot: live, error: null };
1891
+ const local = await readMuseLocalSessionUsage(base);
1892
+ if (!local)
1893
+ return { snapshot: null, error: null };
1894
+ return { snapshot: local, error: null };
1895
+ }
1896
+ catch {
1897
+ return { snapshot: null, error: null };
1898
+ }
1899
+ }
1900
+ /** Resolve a Muse API key from env or auth.json without logging the value. */
1901
+ function resolveMuseApiKey(base) {
1902
+ const envKey = process.env.META_API_KEY?.trim() || process.env.MODEL_API_KEY?.trim();
1903
+ if (envKey)
1904
+ return envKey;
1905
+ const authPath = path.join(base, '.config', 'muse', 'auth.json');
1906
+ try {
1907
+ if (!fs.existsSync(authPath))
1908
+ return null;
1909
+ const data = JSON.parse(fs.readFileSync(authPath, 'utf-8'));
1910
+ if (typeof data.access_token === 'string' && data.access_token)
1911
+ return data.access_token;
1912
+ if (typeof data.api_key === 'string' && data.api_key)
1913
+ return data.api_key;
1914
+ for (const slot of Object.values(data)) {
1915
+ if (!slot || typeof slot !== 'object' || Array.isArray(slot))
1916
+ continue;
1917
+ const entry = slot;
1918
+ if (typeof entry.access_token === 'string' && entry.access_token)
1919
+ return entry.access_token;
1920
+ if (typeof entry.api_key === 'string' && entry.api_key)
1921
+ return entry.api_key;
1922
+ }
1923
+ }
1924
+ catch {
1925
+ /* unreadable auth */
1926
+ }
1927
+ return null;
1928
+ }
1929
+ /**
1930
+ * Probe Meta Model API for rate-limit headers. Uses GET /v1/models (no token
1931
+ * spend). Returns null when unauthenticated or the headers are absent.
1932
+ */
1933
+ async function probeMuseRateLimits(base) {
1934
+ if (usageRateLimitedUntil('muse'))
1935
+ return null;
1936
+ const key = resolveMuseApiKey(base);
1937
+ if (!key)
1938
+ return null;
1939
+ try {
1940
+ const response = await fetch('https://api.meta.ai/v1/models', {
1941
+ headers: { Authorization: `Bearer ${key}` },
1942
+ signal: AbortSignal.timeout(8_000),
1943
+ });
1944
+ if (response.status === 429) {
1945
+ noteUsageRateLimited('muse', response.headers.get('retry-after'));
1946
+ return null;
1947
+ }
1948
+ if (!response.ok)
1949
+ return null;
1950
+ const limitTokens = headerNumber(response.headers, 'x-ratelimit-limit-tokens');
1951
+ const remainingTokens = headerNumber(response.headers, 'x-ratelimit-remaining-tokens');
1952
+ const limitRequests = headerNumber(response.headers, 'x-ratelimit-limit-requests');
1953
+ const remainingRequests = headerNumber(response.headers, 'x-ratelimit-remaining-requests');
1954
+ const windows = [];
1955
+ if (limitTokens !== null && remainingTokens !== null && limitTokens > 0) {
1956
+ const used = Math.max(0, Math.min(100, ((limitTokens - remainingTokens) / limitTokens) * 100));
1957
+ windows.push({
1958
+ key: 'session',
1959
+ label: 'Tokens (current window)',
1960
+ shortLabel: 'Tok',
1961
+ usedPercent: used,
1962
+ resetsAt: null,
1963
+ windowMinutes: 1,
1964
+ });
1965
+ }
1966
+ if (limitRequests !== null && remainingRequests !== null && limitRequests > 0) {
1967
+ const used = Math.max(0, Math.min(100, ((limitRequests - remainingRequests) / limitRequests) * 100));
1968
+ windows.push({
1969
+ key: 'session',
1970
+ label: 'Requests (current window)',
1971
+ shortLabel: 'Req',
1972
+ usedPercent: used,
1973
+ resetsAt: null,
1974
+ windowMinutes: 1,
1975
+ });
1976
+ }
1977
+ if (windows.length === 0)
1978
+ return null;
1979
+ return {
1980
+ source: 'live',
1981
+ sourceLabel: 'Meta Model API rate limits',
1982
+ capturedAt: new Date(),
1983
+ windows,
1984
+ plan: 'Meta Model API',
1985
+ };
1986
+ }
1987
+ catch {
1988
+ return null;
1989
+ }
1990
+ }
1991
+ function headerNumber(headers, name) {
1992
+ const raw = headers.get(name);
1993
+ if (raw === null || raw === '')
1994
+ return null;
1995
+ const n = Number(raw);
1996
+ return Number.isFinite(n) ? n : null;
1997
+ }
1998
+ /**
1999
+ * Aggregate Muse session token usage from local session.jsonl files for the
2000
+ * last 7 days. Scales the bar against 10M tokens (soft visibility scale — Meta
2001
+ * is pay-as-you-go with no hard local cap).
2002
+ */
2003
+ async function readMuseLocalSessionUsage(base) {
2004
+ const root = path.join(base, '.local', 'share', 'muse', 'sessions');
2005
+ if (!fs.existsSync(root))
2006
+ return null;
2007
+ const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1000;
2008
+ let inputTokens = 0;
2009
+ let outputTokens = 0;
2010
+ let cachedTokens = 0;
2011
+ let latestAt = null;
2012
+ let files = 0;
2013
+ const walk = (dir) => {
2014
+ let entries;
2015
+ try {
2016
+ entries = fs.readdirSync(dir, { withFileTypes: true });
2017
+ }
2018
+ catch {
2019
+ return;
2020
+ }
2021
+ for (const ent of entries) {
2022
+ const full = path.join(dir, ent.name);
2023
+ if (ent.isDirectory()) {
2024
+ walk(full);
2025
+ continue;
2026
+ }
2027
+ if (ent.name !== 'session.jsonl')
2028
+ continue;
2029
+ try {
2030
+ const st = fs.statSync(full);
2031
+ if (st.mtimeMs < cutoff)
2032
+ continue;
2033
+ files++;
2034
+ if (!latestAt || st.mtime > latestAt)
2035
+ latestAt = st.mtime;
2036
+ const content = fs.readFileSync(full, 'utf-8');
2037
+ for (const line of content.split('\n')) {
2038
+ if (!line.trim())
2039
+ continue;
2040
+ let raw;
2041
+ try {
2042
+ raw = JSON.parse(line);
2043
+ }
2044
+ catch {
2045
+ continue;
2046
+ }
2047
+ const event = raw?.payload?.event ?? raw?.payload;
2048
+ if (!event || event.kind !== 'model_completed')
2049
+ continue;
2050
+ const usage = event.usage;
2051
+ if (!usage || typeof usage !== 'object')
2052
+ continue;
2053
+ if (typeof usage.input_tokens === 'number')
2054
+ inputTokens += usage.input_tokens;
2055
+ if (typeof usage.output_tokens === 'number')
2056
+ outputTokens += usage.output_tokens;
2057
+ if (typeof usage.cached_tokens === 'number')
2058
+ cachedTokens += usage.cached_tokens;
2059
+ }
2060
+ }
2061
+ catch {
2062
+ /* skip unreadable session */
2063
+ }
2064
+ }
2065
+ };
2066
+ walk(root);
2067
+ const total = inputTokens + outputTokens + cachedTokens;
2068
+ if (total === 0 && files === 0)
2069
+ return null;
2070
+ // Soft 10M-token scale for the bar (pay-as-you-go has no hard local cap).
2071
+ const softCap = 10_000_000;
2072
+ const usedPercent = Math.max(0, Math.min(100, (total / softCap) * 100));
2073
+ const formatK = (n) => n >= 1_000_000 ? `${(n / 1_000_000).toFixed(1)}M` : n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n);
2074
+ return {
2075
+ source: 'last_seen',
2076
+ sourceLabel: `local Muse sessions · ${formatK(total)} tokens (7d)`,
2077
+ capturedAt: latestAt,
2078
+ windows: [
2079
+ {
2080
+ key: 'week',
2081
+ label: 'Local tokens (7d)',
2082
+ shortLabel: '7d',
2083
+ usedPercent,
2084
+ resetsAt: null,
2085
+ windowMinutes: 7 * 24 * 60,
2086
+ },
2087
+ ],
2088
+ plan: 'Meta Model API',
2089
+ };
2090
+ }
1876
2091
  async function readLatestGrokBilling(filePath) {
1877
2092
  return new Promise((resolve) => {
1878
2093
  let latest = null;
@@ -917,6 +917,17 @@ export function getBinaryPath(agent, version) {
917
917
  ? path.join(getHomeDir(), 'bin', 'droid.exe')
918
918
  : path.join(getHomeDir(), '.local', 'bin', 'droid');
919
919
  }
920
+ if (agent === 'muse') {
921
+ // Muse Code install script drops a self-updating launcher at
922
+ // ~/.local/bin/muse (curl -fsSL https://dev.meta.ai/install.sh | sh).
923
+ // Same global-binary shape as droid: one binary for every version dir,
924
+ // config isolation via version-home HOME rewrite. Mirror the shim's
925
+ // `muse` branch so isVersionInstalled / agents view / agents import
926
+ // agree with what executes.
927
+ return IS_WINDOWS
928
+ ? path.join(getHomeDir(), 'bin', 'muse.exe')
929
+ : path.join(getHomeDir(), '.local', 'bin', 'muse');
930
+ }
920
931
  const versionDir = getVersionDir(agent, version);
921
932
  return path.join(versionDir, 'node_modules', '.bin', agentConfig.cliCommand);
922
933
  }
@@ -1521,14 +1532,14 @@ export async function installVersion(agent, version, onProgress, opts) {
1521
1532
  // ~/.local/bin (or similar) rather than the version's node_modules/.bin.
1522
1533
  //
1523
1534
  // Agents whose binary is special-cased in getBinaryPath (grok ->
1524
- // ~/.grok/downloads, droid -> ~/.local/bin/droid) need no symlink — and
1535
+ // ~/.grok/downloads, droid/muse -> ~/.local/bin/<cli>) need no symlink — and
1525
1536
  // creating one is actively harmful: `which <cli>` can resolve to OUR OWN
1526
1537
  // dispatcher shim, because ~/.agents/.cache/shims sits ahead of ~/.local/bin
1527
1538
  // on PATH. Symlinking node_modules/.bin/<cli> at the shim makes the shim
1528
1539
  // exec itself forever. So we skip the resolver-backed agents here AND, for
1529
1540
  // everyone else, filter the shims dir out of the `which` candidates so the
1530
1541
  // same race can't bite a non-special-cased installScript agent.
1531
- if (agent !== 'grok' && agent !== 'droid') {
1542
+ if (agent !== 'grok' && agent !== 'droid' && agent !== 'muse') {
1532
1543
  // findInPath is a pure-Node PATH scan that already skips our own shims
1533
1544
  // dir — so it returns the genuine install, never our dispatcher shim
1534
1545
  // (which sits ahead of ~/.local/bin on PATH and would otherwise be
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phnx-labs/agents-cli",
3
- "version": "1.22.22",
3
+ "version": "1.22.24",
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",