@phnx-labs/agents-cli 1.22.22 → 1.22.23

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 (91) hide show
  1. package/CHANGELOG.md +209 -0
  2. package/README.md +8 -2
  3. package/dist/commands/doctor.js +15 -7
  4. package/dist/commands/exec.js +20 -6
  5. package/dist/commands/focus.d.ts +76 -4
  6. package/dist/commands/focus.js +219 -40
  7. package/dist/commands/fork.d.ts +20 -2
  8. package/dist/commands/fork.js +91 -64
  9. package/dist/commands/go.d.ts +25 -0
  10. package/dist/commands/go.js +63 -2
  11. package/dist/commands/harness-wizard.d.ts +206 -0
  12. package/dist/commands/harness-wizard.js +403 -0
  13. package/dist/commands/harness.d.ts +12 -0
  14. package/dist/commands/harness.js +97 -101
  15. package/dist/commands/resume.js +10 -3
  16. package/dist/commands/secrets.js +25 -30
  17. package/dist/commands/sessions-resume.d.ts +21 -3
  18. package/dist/commands/sessions-resume.js +50 -11
  19. package/dist/commands/sessions.d.ts +81 -5
  20. package/dist/commands/sessions.js +325 -66
  21. package/dist/commands/watchdog.js +13 -2
  22. package/dist/lib/agents.d.ts +1 -1
  23. package/dist/lib/agents.js +132 -0
  24. package/dist/lib/codex-policy.d.ts +17 -0
  25. package/dist/lib/codex-policy.js +48 -0
  26. package/dist/lib/crabbox/lease.d.ts +25 -0
  27. package/dist/lib/crabbox/lease.js +62 -0
  28. package/dist/lib/daemon.js +70 -0
  29. package/dist/lib/exec.d.ts +4 -0
  30. package/dist/lib/exec.js +88 -54
  31. package/dist/lib/feed-broadcast.d.ts +1 -20
  32. package/dist/lib/feed-broadcast.js +31 -1
  33. package/dist/lib/hooks.js +12 -2
  34. package/dist/lib/mcp.js +44 -0
  35. package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +1 -5
  36. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  37. package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +2 -15
  38. package/dist/lib/models.d.ts +0 -5
  39. package/dist/lib/models.js +48 -0
  40. package/dist/lib/plugin-marketplace.js +9 -0
  41. package/dist/lib/pricing/prices.json +119 -92
  42. package/dist/lib/pricing/table.js +13 -0
  43. package/dist/lib/remote-agents-json.d.ts +29 -1
  44. package/dist/lib/remote-agents-json.js +47 -10
  45. package/dist/lib/resources/mcp.js +2 -0
  46. package/dist/lib/resources/permissions.js +3 -0
  47. package/dist/lib/resources/types.d.ts +2 -1
  48. package/dist/lib/runner.js +28 -19
  49. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  50. package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +0 -2
  51. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  52. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +1 -13
  53. package/dist/lib/secrets/index.d.ts +15 -1
  54. package/dist/lib/secrets/index.js +118 -49
  55. package/dist/lib/secrets/reaper.d.ts +87 -0
  56. package/dist/lib/secrets/reaper.js +184 -0
  57. package/dist/lib/secrets/remote.d.ts +29 -0
  58. package/dist/lib/secrets/remote.js +37 -1
  59. package/dist/lib/session/active.d.ts +36 -1
  60. package/dist/lib/session/active.js +60 -19
  61. package/dist/lib/session/actor-sidecar.d.ts +14 -0
  62. package/dist/lib/session/actor-sidecar.js +67 -4
  63. package/dist/lib/session/db.d.ts +1 -1
  64. package/dist/lib/session/db.js +32 -1
  65. package/dist/lib/session/discover.js +168 -0
  66. package/dist/lib/session/parse.d.ts +10 -0
  67. package/dist/lib/session/parse.js +98 -0
  68. package/dist/lib/session/remote-list.d.ts +10 -1
  69. package/dist/lib/session/remote-list.js +2 -8
  70. package/dist/lib/session/remote.d.ts +57 -3
  71. package/dist/lib/session/remote.js +90 -26
  72. package/dist/lib/session/resume-command.d.ts +6 -0
  73. package/dist/lib/session/resume-command.js +8 -0
  74. package/dist/lib/session/session-cache.d.ts +173 -0
  75. package/dist/lib/session/session-cache.js +399 -0
  76. package/dist/lib/session/types.d.ts +1 -1
  77. package/dist/lib/session/types.js +1 -1
  78. package/dist/lib/session/width.d.ts +1 -1
  79. package/dist/lib/session/width.js +12 -2
  80. package/dist/lib/shims.d.ts +2 -2
  81. package/dist/lib/shims.js +40 -5
  82. package/dist/lib/staleness/writers/hooks.js +1 -1
  83. package/dist/lib/types.d.ts +1 -1
  84. package/dist/lib/usage.d.ts +13 -0
  85. package/dist/lib/usage.js +215 -0
  86. package/dist/lib/versions.js +13 -2
  87. package/package.json +1 -1
  88. package/dist/bin/agents +0 -0
  89. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  90. package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
  91. package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
package/dist/lib/exec.js CHANGED
@@ -17,11 +17,11 @@ import { isTierToken, resolveTier } from './model-tiers.js';
17
17
  import { createTimer, redactPrompt, redactArgs } from './events.js';
18
18
  import { sanitizeProcessEnv } from './secrets/bundles.js';
19
19
  import { resolveActor, actorEnv } from './actor.js';
20
- import { getShimsDir, getHistoryDir, getUserAgentsDir } from './state.js';
20
+ import { getShimsDir, getHistoryDir } from './state.js';
21
21
  import { resolveCodexHome } from './codex-home.js';
22
22
  import { readCodexConfiguredModel } from './shims.js';
23
23
  import { writePidSessionEntry, extractSessionIdArg } from './session/pid-registry.js';
24
- import { writeSessionActorRecord } from './session/actor-sidecar.js';
24
+ import { writeSessionActorRecord, writeSessionAliasRecord } from './session/actor-sidecar.js';
25
25
  import { loadHookSessionIndex, resolveHookSessionId } from './session/hook-sessions.js';
26
26
  import { sessionIdMarkerLine } from './hosts/session-marker.js';
27
27
  import { recordRunName } from './session/run-names.js';
@@ -30,6 +30,7 @@ import { composeWin32CommandLine } from './platform/index.js';
30
30
  import { isTmuxInstalled } from './tmux/binary.js';
31
31
  import { shellQuote } from './ssh-exec.js';
32
32
  import { resolveClaudeSetupToken } from './claude-account-token.js';
33
+ import { codexEditWritableRoots, codexPolicyArgs } from './codex-policy.js';
33
34
  /**
34
35
  * Map a raw mode string (CLI flag, YAML field, env var) to the canonical Mode.
35
36
  *
@@ -172,6 +173,10 @@ export function resolveHeadlessMode(agent, requested, interactive, warningContex
172
173
  export function defaultModeFor(agent) {
173
174
  return AGENTS[agent].capabilities.modes[0];
174
175
  }
176
+ /** Safe mode used when the user did not provide --mode or a configured default. */
177
+ export function implicitModeFor(agent) {
178
+ return agent === 'codex' ? 'edit' : 'plan';
179
+ }
175
180
  /**
176
181
  * Resolve interactive vs headless. Explicit flags are definitive and win over
177
182
  * inference: `--interactive` forces interactive, `--headless` forces headless.
@@ -441,11 +446,11 @@ export const AGENT_COMMANDS = {
441
446
  promptFlag: 'positional',
442
447
  resume: { subcommand: 'resume' },
443
448
  modeFlags: {
444
- plan: ['--sandbox', 'read-only'],
445
- // Sandboxed writes inside the workspace. Network stays on (workspace-write
446
- // disables it by default) so edit-mode runs can still use git/gh/package
447
- // installs. No approval bypass here — only `skip` drops the guardrails.
448
- edit: ['--sandbox', 'workspace-write', '-c', 'sandbox_workspace_write.network_access=true'],
449
+ // Native Codex modes are assembled by codexPolicyArgs below. Named
450
+ // permission profiles keep filesystem access and network access
451
+ // independent; legacy --sandbox flags cannot express that combination.
452
+ plan: [],
453
+ edit: [],
449
454
  // skip = codex --yolo: drops the sandbox entirely and approves anything.
450
455
  skip: ['--dangerously-bypass-approvals-and-sandbox'],
451
456
  },
@@ -637,6 +642,29 @@ export const AGENT_COMMANDS = {
637
642
  },
638
643
  modelFlag: '--model',
639
644
  },
645
+ // Meta Muse Code (`muse exec` headless, `muse` TUI). Flags from
646
+ // `muse --help` / `muse exec --help` (v0.1.0): prompt is positional;
647
+ // --model selects muse-spark-*; --reasoning-effort is the effort dial;
648
+ // --disable-write approximates plan (no non-shell writes); --disable-approval
649
+ // keeps the sandbox (auto); --yolo drops approval+sandbox and trusts the
650
+ // workspace (skip). --json emits JSONL on stdout. Interactive resume is
651
+ // `muse resume <id>`; headless resume is `muse exec --session-id <id>` —
652
+ // see the muse resume branch in buildExecCommand.
653
+ muse: {
654
+ base: ['muse', 'exec'],
655
+ promptFlag: 'positional',
656
+ modeFlags: {
657
+ plan: ['--disable-write'],
658
+ edit: [],
659
+ auto: ['--disable-approval'],
660
+ skip: ['--yolo'],
661
+ },
662
+ jsonFlags: ['--json'],
663
+ modelFlag: '--model',
664
+ // Flag form covers headless (`--session-id`). Interactive uses the
665
+ // `resume` subcommand — special-cased in buildExecCommand.
666
+ resume: { flag: '--session-id' },
667
+ },
640
668
  };
641
669
  /**
642
670
  * Whether `agent` has a native resume form (Tier 1). Derived solely from the
@@ -665,12 +693,13 @@ export function buildExecCommand(options) {
665
693
  const template = AGENT_COMMANDS[options.agent];
666
694
  const cmd = [...template.base];
667
695
  const interactive = resolveInteractive(options);
668
- // For Codex and Droid, 'exec' is the headless subcommand; for OpenCode, 'run'
669
- // is. Drop it for interactive mode so we launch the TUI (`codex` / `droid` /
670
- // `opencode`, each agent's default command) instead of the one-shot headless
671
- // subcommand ('codex exec' / 'droid exec' / 'opencode run').
696
+ // For Codex, Droid, and Muse, 'exec' is the headless subcommand; for OpenCode,
697
+ // 'run' is. Drop it for interactive mode so we launch the TUI (`codex` /
698
+ // `droid` / `muse` / `opencode`) instead of the one-shot headless subcommand
699
+ // ('codex exec' / 'droid exec' / 'muse exec' / 'opencode run').
672
700
  if (interactive) {
673
- if ((options.agent === 'codex' || options.agent === 'droid') && cmd[1] === 'exec') {
701
+ if ((options.agent === 'codex' || options.agent === 'droid' || options.agent === 'muse') &&
702
+ cmd[1] === 'exec') {
674
703
  cmd.splice(1, 1);
675
704
  }
676
705
  else if (options.agent === 'opencode' && cmd[1] === 'run') {
@@ -682,8 +711,18 @@ export function buildExecCommand(options) {
682
711
  // interactive drop above, `codex` -> `codex resume` (TUI). The session id is
683
712
  // pushed later as the first positional (before any prompt). `{ flag }` agents
684
713
  // (claude) need no base change — the flag is appended with the id below.
714
+ //
715
+ // Muse interactive resume is also a subcommand (`muse resume <id>`), but the
716
+ // session id MUST sit immediately after the verb — mode/model flags come
717
+ // after. So for interactive muse we push both `resume` and the id here, and
718
+ // skip the later generic resume flag path.
685
719
  const resumeSpec = options.resume ? template.resume : undefined;
686
- if (resumeSpec && 'subcommand' in resumeSpec) {
720
+ let museInteractiveResumeDone = false;
721
+ if (options.agent === 'muse' && options.resume && interactive && options.sessionId) {
722
+ cmd.push('resume', options.sessionId);
723
+ museInteractiveResumeDone = true;
724
+ }
725
+ else if (resumeSpec && 'subcommand' in resumeSpec) {
687
726
  cmd.push(resumeSpec.subcommand);
688
727
  }
689
728
  // Use versioned alias if a specific version was requested (e.g., claude@2.1.98).
@@ -764,40 +803,26 @@ export function buildExecCommand(options) {
764
803
  // narrower than --yolo/-f, which also bypasses permission checks.
765
804
  cmd.push('--trust');
766
805
  }
767
- // Codex's workspace-write sandbox blocks $HOME (verified against the live CLI
768
- // and OpenAI's sandbox docs: writable roots extend scope "without removing the
769
- // sandbox entirely"). But the model routinely shells out to `agents ...`, whose
770
- // runtime state lives under ~/.agents — the SSH askpass shim
771
- // (~/.agents/.cache/devices/askpass.sh), the device/stats cache, secrets,
772
- // session writes, config tunings. Without ~/.agents as a writable root those
773
- // inner writes fail with EROFS (e.g. `agents ssh` dies before connecting),
774
- // which is why a remote `agents run codex` couldn't reach the fleet. Grant it
775
- // implicitly whenever codex runs workspace-write — far narrower than --mode skip
776
- // (danger-full-access). Fresh runs take --add-dir (below); resume forms reject
777
- // --add-dir, so they take the same root via -c writable_roots here.
778
- const codexWorkspaceWrite = options.agent === 'codex' && resolvedMode === 'edit';
779
- if (resumeSpec && 'subcommand' in resumeSpec) {
806
+ if (options.agent === 'codex') {
807
+ const policyMode = resolvedMode === 'plan' || resolvedMode === 'skip' ? resolvedMode : 'edit';
808
+ const writableRoots = [...codexEditWritableRoots(), ...(options.addDirs ?? [])];
809
+ cmd.push(...codexPolicyArgs(policyMode, writableRoots));
810
+ }
811
+ else if (resumeSpec && 'subcommand' in resumeSpec) {
780
812
  if (resolvedMode === 'skip') {
781
813
  // skip = yolo on resume too; both `codex resume` (TUI) and
782
814
  // `codex exec resume` accept the bypass flag.
783
815
  cmd.push('--dangerously-bypass-approvals-and-sandbox');
784
816
  }
785
817
  else if (interactive) {
786
- // `codex resume` (TUI) accepts the same -s/--sandbox flags as a fresh run.
787
818
  cmd.push(...modeFlags);
788
- if (codexWorkspaceWrite)
789
- cmd.push('-c', codexWritableRootsConfig(getUserAgentsDir()));
790
819
  }
791
820
  else {
792
821
  // `codex exec resume` rejects `--sandbox <mode>` (verified against
793
822
  // `codex exec resume --help` on 0.142.5), but takes -c config overrides —
794
823
  // map the mode through sandbox_mode so a non-skip resume never gets the
795
824
  // approval/sandbox bypass.
796
- cmd.push('-c', `sandbox_mode=${resolvedMode === 'plan' ? 'read-only' : 'workspace-write'}`);
797
- if (resolvedMode !== 'plan') {
798
- cmd.push('-c', 'sandbox_workspace_write.network_access=true');
799
- cmd.push('-c', codexWritableRootsConfig(getUserAgentsDir()));
800
- }
825
+ cmd.push(...modeFlags);
801
826
  }
802
827
  }
803
828
  else if (options.agent === 'kimi' && !interactive) {
@@ -835,7 +860,7 @@ export function buildExecCommand(options) {
835
860
  // agents (codex) already pushed the verb above, so the id is the first
836
861
  // positional here — placed before the prompt positional appended later. Without
837
862
  // `resume`, the legacy claude-only `--session-id` CREATES a session with that id.
838
- if (options.resume && options.sessionId && resumeSpec) {
863
+ if (options.resume && options.sessionId && resumeSpec && !museInteractiveResumeDone) {
839
864
  if ('flag' in resumeSpec) {
840
865
  const flag = interactive
841
866
  ? (resumeSpec.interactiveFlag ?? resumeSpec.flag)
@@ -913,21 +938,10 @@ export function buildExecCommand(options) {
913
938
  cmd.push(template.promptFlag, options.prompt);
914
939
  }
915
940
  }
916
- // Extra writable dirs. Claude and Codex both take `--add-dir`; for Codex it
917
- // widens the workspace-write sandbox (teams relies on this to let teammates
918
- // write ~/.agents), so it must actually be forwarded — it used to be
919
- // claude-only, silently dropped for codex and masked by edit mode carrying
920
- // the approval/sandbox bypass. Codex's resume forms reject --add-dir, so
921
- // skip it there (claude's flag-based resume accepts it).
922
- //
923
- // On top of any user-supplied dirs, a fresh codex workspace-write run
924
- // implicitly gets ~/.agents (deduped) so the CLI's own tooling — askpass,
925
- // secrets, sessions, tunings — can write from inside the sandbox. Resume forms
926
- // get the same root via -c writable_roots above (see codexWorkspaceWrite).
927
- const codexImplicitDirs = codexWorkspaceWrite && !options.resume ? [getUserAgentsDir()] : [];
928
- const addDirs = [...new Set([...(options.addDirs ?? []), ...codexImplicitDirs])];
929
- if (addDirs.length > 0 &&
930
- (options.agent === 'claude' || (options.agent === 'codex' && !options.resume))) {
941
+ // Codex add-dirs are folded into its named edit profile above, including on
942
+ // resume where the native CLI rejects --add-dir. Claude keeps its native flag.
943
+ const addDirs = [...new Set(options.addDirs ?? [])];
944
+ if (addDirs.length > 0 && options.agent === 'claude') {
931
945
  for (const dir of addDirs) {
932
946
  cmd.push('--add-dir', dir);
933
947
  }
@@ -1024,8 +1038,11 @@ export async function execShimPassthrough(agent, rawArgs, cwd, pinnedVersion) {
1024
1038
  if (fs.existsSync(cmdPath))
1025
1039
  binary = cmdPath;
1026
1040
  }
1027
- // The only flag the bash shim injects (codex); everything else is transparent.
1028
- const launchArgs = agent === 'codex' ? ['-c', 'check_for_update_on_startup=false'] : [];
1041
+ // Match the POSIX shim: direct Codex launches default to the safe writable
1042
+ // profile, while later user arguments can still override native settings.
1043
+ const launchArgs = agent === 'codex'
1044
+ ? ['-c', 'check_for_update_on_startup=false', ...codexPolicyArgs('edit')]
1045
+ : [];
1029
1046
  // Mint a launch id and export it as AGENT_LAUNCH_ID so the agent's SessionStart
1030
1047
  // hook records the same id — the join key that maps this launch to its exact
1031
1048
  // session even though the recorded pid here is the cmd.exe wrapper (Windows) or
@@ -1192,14 +1209,28 @@ export function formatPaneTail(raw, maxLines = 30) {
1192
1209
  * (Ctrl-b d) — return 0 and LEAVE the session for `agents focus` to re-attach.
1193
1210
  */
1194
1211
  async function runInTmux(options, executable, args) {
1195
- const { createSession, killSession, paneExitStatus, setSessionHook, slugifyName, agentPaneDiedHook, markSessionHookSchema } = await import('./tmux/session.js');
1212
+ const { createSession, hasSession, killSession, paneExitStatus, setSessionHook, slugifyName, agentPaneDiedHook, markSessionHookSchema } = await import('./tmux/session.js');
1196
1213
  const { getDefaultSocketPath } = await import('./tmux/paths.js');
1197
1214
  const { attachTmux, runTmux } = await import('./tmux/binary.js');
1198
1215
  const socket = getDefaultSocketPath();
1199
1216
  const cwd = options.cwd || process.cwd();
1200
1217
  const idSeed = (options.sessionId ?? randomUUID()).slice(0, 8);
1201
1218
  const name = slugifyName(`ag-${options.agent}-${idSeed}`);
1202
- const execEnv = buildExecEnv(options);
1219
+ // A native resume must not create a competing wrapper for a live session.
1220
+ // A retained dead pane is reaped before the harness resumes normally.
1221
+ if (options.resume && await hasSession(name, socket)) {
1222
+ const existing = await createSession({ name, cwd, socket, source: 'cli', attachExisting: true });
1223
+ if (options.sessionId)
1224
+ writeSessionAliasRecord(options.sessionId, name);
1225
+ if (!existing.pane || !(await paneExitStatus(existing.pane, socket)).dead) {
1226
+ await attachTmux({ socket, args: ['attach-session', '-t', name] });
1227
+ return { exitCode: 0, stderr: '', stdout: '' };
1228
+ }
1229
+ await killSession(name, socket);
1230
+ }
1231
+ // SessionStart learns some harness IDs only after launch. Carry the wrapper
1232
+ // name into that hook so it can bind both identities durably.
1233
+ const execEnv = { ...buildExecEnv(options), AGENT_TMUX_SESSION_NAME: name };
1203
1234
  // Launch with the real env (secret VALUES materialized into the pane); persist
1204
1235
  // only a value-redacted copy in SessionMeta.cmd so resolved secrets never hit
1205
1236
  // disk via the informational cmd field (RUSH-1758).
@@ -1210,6 +1241,8 @@ async function runInTmux(options, executable, args) {
1210
1241
  labels.sessionId = options.sessionId;
1211
1242
  const meta = await createSession({ name, cmd, metaCmd, cwd, socket, source: 'cli', labels });
1212
1243
  const pane = meta.pane;
1244
+ if (options.sessionId)
1245
+ writeSessionAliasRecord(options.sessionId, name);
1213
1246
  if (pane) {
1214
1247
  // When the AGENT pane dies, detach the client (don't kill) so the session
1215
1248
  // survives just long enough to read the dead pane's exit status below. The
@@ -1873,6 +1906,7 @@ export async function runWithFallback(options) {
1873
1906
  ...options,
1874
1907
  agent,
1875
1908
  version,
1909
+ mode: options.modeWasImplicit ? implicitModeFor(agent) : options.mode,
1876
1910
  prompt,
1877
1911
  env: envOverride ? { ...(options.env ?? {}), ...envOverride } : options.env,
1878
1912
  sessionId: pinnedSessionId ?? (i === 0 ? options.sessionId : undefined),
@@ -141,26 +141,7 @@ export declare function scrubOutboundDashes(text: string): string;
141
141
  * host last. Skip the uninformative default label `agent`.
142
142
  */
143
143
  export declare function composeBroadcastFooter(ctx: FeedBroadcastContext): string | undefined;
144
- /**
145
- * Human-facing body for a messaging sink (`{message}`).
146
- *
147
- * ```
148
- * Title in a few words
149
- *
150
- * Body of what happened or the ask.
151
- * Options: publish / wait (blocks with choices)
152
- * Default in 15 min: wait (blocks with a safe default)
153
- *
154
- * Sent from grok/a02da0e2 on mac-mini
155
- * https://… (optional attach URL)
156
- * ```
157
- *
158
- * Title first (scannable subject). Blank line. Body. Then the phone-actionable
159
- * choices + default (a block that has stopped for the human), then footer
160
- * provenance. No `agents focus <id>` line: a CLI command is unusable from a phone,
161
- * so the safe default is the fallback and the message carries it. Prefer `{message}`
162
- * over bare `{text}` in messaging sinks.
163
- */
144
+ export declare function truncateBroadcastBody(body: string): string;
164
145
  export declare function composeBroadcastMessage(ctx: FeedBroadcastContext): string;
165
146
  /**
166
147
  * Substitute `{placeholder}` tokens in an argv template. Returns undefined when
@@ -202,9 +202,39 @@ export function composeBroadcastFooter(ctx) {
202
202
  * so the safe default is the fallback and the message carries it. Prefer `{message}`
203
203
  * over bare `{text}` in messaging sinks.
204
204
  */
205
+ /**
206
+ * The phone copy is a text, not a report. A long body reaches the owner's phone
207
+ * as an unreadable wall, and the `feed post` forwarding path is where that has to
208
+ * be shaped: every sink (owner alias, in-process `channel:`, spawned `command:`)
209
+ * funnels through {@link composeBroadcastMessage}, which is the only seam a shell
210
+ * hook cannot reach. Keep the title (the scannable headline) and cap the BODY to a
211
+ * short excerpt, marking the cut with a plain "(full in feed)" — NOT a CLI command
212
+ * (unusable from a phone, see the note on composeBroadcastMessage). Nothing is
213
+ * lost: this shapes only the outbound sink text; the full post stays in the feed.
214
+ */
215
+ const PHONE_BODY_MAX_CHARS = 500;
216
+ const PHONE_BODY_MAX_LINES = 8;
217
+ export function truncateBroadcastBody(body) {
218
+ if (!body)
219
+ return body;
220
+ let out = body;
221
+ let cut = false;
222
+ const lines = out.split('\n');
223
+ if (lines.length > PHONE_BODY_MAX_LINES) {
224
+ out = lines.slice(0, PHONE_BODY_MAX_LINES).join('\n');
225
+ cut = true;
226
+ }
227
+ if (out.length > PHONE_BODY_MAX_CHARS) {
228
+ out = out.slice(0, PHONE_BODY_MAX_CHARS);
229
+ cut = true;
230
+ }
231
+ if (!cut)
232
+ return body;
233
+ return `${out.trimEnd()}\n… (full in feed)`;
234
+ }
205
235
  export function composeBroadcastMessage(ctx) {
206
236
  const title = scrubOutboundDashes(ctx.title ?? '');
207
- const body = scrubOutboundDashes(ctx.text ?? '');
237
+ const body = truncateBroadcastBody(scrubOutboundDashes(ctx.text ?? ''));
208
238
  // Title preferred; if an older post has no title, body alone still sends.
209
239
  const head = title || body;
210
240
  const mid = title && body && title !== body ? body : undefined;
package/dist/lib/hooks.js CHANGED
@@ -758,7 +758,7 @@ export function listHooksInVersionHome(agent, version) {
758
758
  * map (Gemini/Antigravity settings.json variants, Codex config.toml, the OpenCode
759
759
  * plugin, …), so it reports them unsupported rather than risk a false verdict.
760
760
  */
761
- const SETTINGS_JSON_HOOK_FAMILY = ['claude', 'droid'];
761
+ const SETTINGS_JSON_HOOK_FAMILY = ['claude', 'droid', 'muse'];
762
762
  /**
763
763
  * Verify that every hook the manifest says should be wired for a (claude|droid)
764
764
  * version is actually REFERENCED in that version's native settings.json — not
@@ -1453,6 +1453,12 @@ export function registerHooksToSettings(agentId, versionHome, hookManifest, agen
1453
1453
  // registrar targeting `.factory/settings.json` (agentConfigDirName('droid')).
1454
1454
  return registerHooksForClaude(versionHome, manifest, resolveScript, managedPrefixes, agentConfigDirName('droid'));
1455
1455
  }
1456
+ if (agentId === 'muse') {
1457
+ // Muse Code: Claude-compatible hooks block in ~/.config/muse/settings.json
1458
+ // (events SessionStart / PreToolUse / …, matcher groups, command hooks).
1459
+ // settings.json MUST carry schema_version: 1 or Muse refuses to start.
1460
+ return registerHooksForClaude(versionHome, manifest, resolveScript, managedPrefixes, agentConfigDirName('muse'), { schemaVersion: 1 });
1461
+ }
1456
1462
  if (agentId === 'codex') {
1457
1463
  return registerHooksForCodex(versionHome, manifest, resolveScript, managedPrefixes);
1458
1464
  }
@@ -1675,7 +1681,7 @@ const ANTIGRAVITY_EVENT_MAP = {
1675
1681
  Stop: 'on_loop_stop',
1676
1682
  OnError: 'on_error',
1677
1683
  };
1678
- function registerHooksForClaude(versionHome, manifest, resolveScript, managedPrefixes, configDirName = '.claude') {
1684
+ function registerHooksForClaude(versionHome, manifest, resolveScript, managedPrefixes, configDirName = '.claude', options) {
1679
1685
  const registered = [];
1680
1686
  const errors = [];
1681
1687
  const configDir = path.join(versionHome, configDirName);
@@ -1692,6 +1698,10 @@ function registerHooksForClaude(versionHome, manifest, resolveScript, managedPre
1692
1698
  return { registered, errors };
1693
1699
  }
1694
1700
  }
1701
+ // Muse (and any future agent that pins a settings schema) requires this key.
1702
+ if (options?.schemaVersion !== undefined && config.schema_version === undefined) {
1703
+ config.schema_version = options.schemaVersion;
1704
+ }
1695
1705
  if (!config.hooks || typeof config.hooks !== 'object') {
1696
1706
  config.hooks = {};
1697
1707
  }
package/dist/lib/mcp.js CHANGED
@@ -595,6 +595,7 @@ function writeMcpConfigSupportsAgent(agentId) {
595
595
  case 'opencode':
596
596
  case 'hermes':
597
597
  case 'pi':
598
+ case 'muse':
598
599
  return true;
599
600
  default:
600
601
  return false;
@@ -797,6 +798,49 @@ export function writeMcpConfig(agentId, configPath, servers, mode = 'overwrite')
797
798
  fs.writeFileSync(configPath, yaml.stringify(config), 'utf-8');
798
799
  break;
799
800
  }
801
+ case 'muse': {
802
+ // Muse Code settings.json: requires schema_version: 1; MCP lives under
803
+ // mcp_servers with an explicit transport (stdio | streamable_http).
804
+ // See https://dev.meta.ai/docs/muse-code/extending#mcp
805
+ let config = {};
806
+ if (fs.existsSync(configPath)) {
807
+ try {
808
+ config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
809
+ }
810
+ catch {
811
+ config = {};
812
+ }
813
+ }
814
+ if (config.schema_version === undefined) {
815
+ config.schema_version = 1;
816
+ }
817
+ const mcpServers = mode === 'merge' && config.mcp_servers && typeof config.mcp_servers === 'object' && !Array.isArray(config.mcp_servers)
818
+ ? { ...config.mcp_servers }
819
+ : {};
820
+ for (const server of servers) {
821
+ if (server.transport === 'stdio') {
822
+ mcpServers[server.name] = {
823
+ transport: 'stdio',
824
+ command: server.command,
825
+ args: server.args || [],
826
+ ...(server.env && Object.keys(server.env).length > 0 ? { env: server.env } : {}),
827
+ enabled: true,
828
+ };
829
+ }
830
+ else {
831
+ mcpServers[server.name] = {
832
+ transport: 'streamable_http',
833
+ url: server.url,
834
+ ...(server.headers && Object.keys(server.headers).length > 0 ? { headers: server.headers } : {}),
835
+ enabled: true,
836
+ };
837
+ }
838
+ }
839
+ config.mcp_servers = mcpServers;
840
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
841
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
842
+ break;
843
+ }
800
844
  }
801
845
  }
802
846
  /**
@@ -7,11 +7,7 @@
7
7
  <key>CFBundleIdentifier</key>
8
8
  <string>com.phnx-labs.agents-menubar</string>
9
9
  <key>CFBundleName</key>
10
- <string>AGI Menu</string>
11
- <key>CFBundleDisplayName</key>
12
- <string>AGI Menu</string>
13
- <key>CFBundleIconFile</key>
14
- <string>AppIcon</string>
10
+ <string>Agents Menu Bar</string>
15
11
  <key>CFBundlePackageType</key>
16
12
  <string>APPL</string>
17
13
  <key>CFBundleShortVersionString</key>
@@ -3,22 +3,9 @@
3
3
  <plist version="1.0">
4
4
  <dict>
5
5
  <key>files</key>
6
- <dict>
7
- <key>Resources/AppIcon.icns</key>
8
- <data>
9
- jOjZVimcFRHoP2VPzgn8uM8mjkA=
10
- </data>
11
- </dict>
6
+ <dict/>
12
7
  <key>files2</key>
13
- <dict>
14
- <key>Resources/AppIcon.icns</key>
15
- <dict>
16
- <key>hash2</key>
17
- <data>
18
- GFvSLeNYJ3ASxW3OzcuiB4aID0gZUBkpozmWZkbTFNw=
19
- </data>
20
- </dict>
21
- </dict>
8
+ <dict/>
22
9
  <key>rules</key>
23
10
  <dict>
24
11
  <key>^Resources/</key>
@@ -97,11 +97,6 @@ export declare function parseGrokModelsStdout(stdout: string): {
97
97
  models: ModelInfo[];
98
98
  aliases: Record<string, string>;
99
99
  };
100
- /**
101
- * Build (or load from cache) the model catalog for a specific (agent, version).
102
- * Cache is keyed on source-file mtime (binary or js module), so re-extracts
103
- * automatically when the user upgrades or reinstalls a version.
104
- */
105
100
  export declare function getModelCatalog(agent: AgentId, version: string): ModelCatalog | null;
106
101
  /** The result of resolving a user-supplied model string against the catalog. */
107
102
  export interface ResolvedModel {
@@ -246,6 +246,16 @@ export function locateModelSource(agent, version) {
246
246
  return { path: pathBin, kind: 'cli' };
247
247
  return null;
248
248
  }
249
+ if (agent === 'muse') {
250
+ // Muse Code is a self-updating native binary on PATH. It has no catalog
251
+ // CLI; the published model IDs are fixed by Meta Model API docs. Point at
252
+ // the binary so the cache key moves with upgrades, and extract a static
253
+ // catalog in getModelCatalog.
254
+ const pathBin = findOnPath('muse');
255
+ if (pathBin)
256
+ return { path: pathBin, kind: 'cli' };
257
+ return null;
258
+ }
249
259
  return null;
250
260
  }
251
261
  /** Real Grok binaries are ~100MB+; failed-download stubs are tens of bytes. */
@@ -973,6 +983,36 @@ function extractPiCatalog(binaryPath) {
973
983
  * Cache is keyed on source-file mtime (binary or js module), so re-extracts
974
984
  * automatically when the user upgrades or reinstalls a version.
975
985
  */
986
+ /**
987
+ * Static Muse Spark catalog. Meta Model API publishes these IDs; Muse Code
988
+ * has no `muse models` command. Default is muse-spark-1.2 (docs + first-run).
989
+ * Source: https://dev.meta.ai/docs/pricing-rate-limits and muse-code overview.
990
+ */
991
+ function extractMuseCatalog() {
992
+ const models = [
993
+ {
994
+ id: 'muse-spark-1.2',
995
+ displayName: 'Muse Spark 1.2',
996
+ isDefault: true,
997
+ },
998
+ {
999
+ id: 'muse-spark-1.1',
1000
+ displayName: 'Muse Spark 1.1',
1001
+ },
1002
+ {
1003
+ id: 'muse-spark-1.2-contributor',
1004
+ displayName: 'Muse Spark 1.2 Contributor',
1005
+ },
1006
+ ];
1007
+ const aliases = {
1008
+ spark: 'muse-spark-1.2',
1009
+ 'spark-1.2': 'muse-spark-1.2',
1010
+ 'spark-1.1': 'muse-spark-1.1',
1011
+ contributor: 'muse-spark-1.2-contributor',
1012
+ default: 'muse-spark-1.2',
1013
+ };
1014
+ return { models, aliases };
1015
+ }
976
1016
  export function getModelCatalog(agent, version) {
977
1017
  const src = locateModelSource(agent, version);
978
1018
  if (!src)
@@ -1027,6 +1067,8 @@ export function getModelCatalog(agent, version) {
1027
1067
  ({ models, aliases } = extractGrokCatalog(src.path));
1028
1068
  else if (agent === 'pi')
1029
1069
  ({ models, aliases } = extractPiCatalog(src.path));
1070
+ else if (agent === 'muse')
1071
+ ({ models, aliases } = extractMuseCatalog());
1030
1072
  }
1031
1073
  // Attach per-token pricing where the offline table knows the model, so the
1032
1074
  // catalog carries $/token for the tier display and budgeting. Subscription /
@@ -1237,5 +1279,11 @@ export function buildReasoningFlags(agent, level) {
1237
1279
  const grokLevel = (normalized === 'xhigh' || normalized === 'max') ? 'high' : normalized;
1238
1280
  return ['--reasoning-effort', grokLevel];
1239
1281
  }
1282
+ if (agent === 'muse') {
1283
+ // Muse Code: `--reasoning-effort none|minimal|low|medium|high|xhigh|ultra`.
1284
+ // Map our unified `max` to Muse's `ultra` (client-side multi-agent aggression).
1285
+ const museLevel = normalized === 'max' ? 'ultra' : normalized;
1286
+ return ['--reasoning-effort', museLevel];
1287
+ }
1240
1288
  return [];
1241
1289
  }
@@ -121,6 +121,11 @@ function dirExists(p) {
121
121
  }
122
122
  // ─── Per-version paths ────────────────────────────────────────────────────────
123
123
  function pluginsRootForVersion(agent, versionHome) {
124
+ // Muse Code keeps its plugin store under XDG data (`~/.local/share/muse/plugins`),
125
+ // not under the config dir. Under HOME isolation the version home is $HOME.
126
+ if (agent === 'muse') {
127
+ return path.join(versionHome, '.local', 'share', 'muse', 'plugins');
128
+ }
124
129
  return path.join(versionHome, agentConfigDirName(agent), 'plugins');
125
130
  }
126
131
  export function marketplaceRoot(specOrName, agent, versionHome) {
@@ -678,6 +683,10 @@ export function addPluginToSettings(pluginName, marketplaceName, agent, versionH
678
683
  settings = {};
679
684
  }
680
685
  }
686
+ // Muse refuses to start without schema_version: 1.
687
+ if (agent === 'muse' && settings.schema_version === undefined) {
688
+ settings.schema_version = 1;
689
+ }
681
690
  if (!settings.enabledPlugins || typeof settings.enabledPlugins !== 'object') {
682
691
  settings.enabledPlugins = {};
683
692
  }