@phnx-labs/agents-cli 1.20.72 → 1.20.74

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 (234) hide show
  1. package/CHANGELOG.md +395 -0
  2. package/README.md +40 -4
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/activity.d.ts +12 -0
  5. package/dist/commands/activity.js +68 -0
  6. package/dist/commands/apply.d.ts +13 -0
  7. package/dist/commands/apply.js +28 -3
  8. package/dist/commands/browser.js +1 -1
  9. package/dist/commands/cloud.js +21 -11
  10. package/dist/commands/doctor.js +21 -5
  11. package/dist/commands/events.d.ts +11 -10
  12. package/dist/commands/events.js +30 -21
  13. package/dist/commands/exec.d.ts +34 -0
  14. package/dist/commands/exec.js +337 -39
  15. package/dist/commands/export.d.ts +11 -0
  16. package/dist/commands/export.js +215 -0
  17. package/dist/commands/feed.d.ts +13 -0
  18. package/dist/commands/feed.js +130 -29
  19. package/dist/commands/hq.d.ts +2 -0
  20. package/dist/commands/hq.js +58 -0
  21. package/dist/commands/import.js +51 -3
  22. package/dist/commands/lease.d.ts +19 -0
  23. package/dist/commands/lease.js +138 -11
  24. package/dist/commands/login.d.ts +2 -0
  25. package/dist/commands/login.js +123 -0
  26. package/dist/commands/logs.js +1 -1
  27. package/dist/commands/mine.d.ts +27 -0
  28. package/dist/commands/mine.js +207 -0
  29. package/dist/commands/monitors.js +79 -49
  30. package/dist/commands/plugins.js +1 -0
  31. package/dist/commands/profiles.js +158 -0
  32. package/dist/commands/repo.js +2 -60
  33. package/dist/commands/resources.d.ts +5 -0
  34. package/dist/commands/resources.js +95 -0
  35. package/dist/commands/routines.d.ts +2 -0
  36. package/dist/commands/routines.js +139 -37
  37. package/dist/commands/secrets.js +234 -47
  38. package/dist/commands/send.d.ts +15 -0
  39. package/dist/commands/send.js +62 -0
  40. package/dist/commands/sessions-tail.d.ts +1 -0
  41. package/dist/commands/sessions-tail.js +20 -9
  42. package/dist/commands/sessions.js +18 -3
  43. package/dist/commands/setup-mine.d.ts +18 -0
  44. package/dist/commands/setup-mine.js +106 -0
  45. package/dist/commands/setup-share.js +2 -2
  46. package/dist/commands/setup.js +9 -2
  47. package/dist/commands/share.d.ts +8 -1
  48. package/dist/commands/share.js +122 -37
  49. package/dist/commands/ssh.d.ts +9 -0
  50. package/dist/commands/ssh.js +196 -26
  51. package/dist/commands/sync.js +1 -1
  52. package/dist/commands/teams.js +11 -2
  53. package/dist/commands/versions.js +36 -5
  54. package/dist/commands/view.js +75 -27
  55. package/dist/index.js +80 -10
  56. package/dist/lib/activity.d.ts +124 -0
  57. package/dist/lib/activity.js +542 -0
  58. package/dist/lib/agents.d.ts +35 -0
  59. package/dist/lib/agents.js +79 -6
  60. package/dist/lib/artifact-actions.d.ts +1 -1
  61. package/dist/lib/artifact-actions.js +1 -1
  62. package/dist/lib/ask-classifier.d.ts +2 -1
  63. package/dist/lib/ask-classifier.js +3 -3
  64. package/dist/lib/auth-health.d.ts +32 -4
  65. package/dist/lib/auth-health.js +40 -1
  66. package/dist/lib/auto-pull.js +8 -1
  67. package/dist/lib/brand.d.ts +40 -0
  68. package/dist/lib/brand.js +122 -0
  69. package/dist/lib/browser/drivers/ssh.js +4 -7
  70. package/dist/lib/browser/service.js +6 -8
  71. package/dist/lib/channels/providers/index.d.ts +2 -0
  72. package/dist/lib/channels/providers/index.js +20 -0
  73. package/dist/lib/channels/providers/mailbox.d.ts +2 -0
  74. package/dist/lib/channels/providers/mailbox.js +26 -0
  75. package/dist/lib/channels/providers/openclaw-telegram.d.ts +2 -0
  76. package/dist/lib/channels/providers/openclaw-telegram.js +34 -0
  77. package/dist/lib/channels/providers/rush.d.ts +6 -0
  78. package/dist/lib/channels/providers/rush.js +55 -0
  79. package/dist/lib/channels/registry.d.ts +42 -0
  80. package/dist/lib/channels/registry.js +20 -0
  81. package/dist/lib/channels/resolve.d.ts +11 -0
  82. package/dist/lib/channels/resolve.js +12 -0
  83. package/dist/lib/cli-resources.d.ts +18 -0
  84. package/dist/lib/cli-resources.js +53 -4
  85. package/dist/lib/cloud/codex.js +6 -3
  86. package/dist/lib/cloud/factory.d.ts +1 -0
  87. package/dist/lib/cloud/factory.js +10 -4
  88. package/dist/lib/cloud/rush.d.ts +3 -1
  89. package/dist/lib/cloud/rush.js +6 -1
  90. package/dist/lib/cloud/types.d.ts +2 -0
  91. package/dist/lib/codex-home.d.ts +35 -0
  92. package/dist/lib/codex-home.js +136 -0
  93. package/dist/lib/config-transfer.d.ts +16 -0
  94. package/dist/lib/config-transfer.js +56 -0
  95. package/dist/lib/crabbox/cli.d.ts +44 -0
  96. package/dist/lib/crabbox/cli.js +120 -20
  97. package/dist/lib/crabbox/lease.d.ts +52 -1
  98. package/dist/lib/crabbox/lease.js +117 -16
  99. package/dist/lib/crabbox/progress.d.ts +31 -0
  100. package/dist/lib/crabbox/progress.js +76 -0
  101. package/dist/lib/crabbox/runtimes.d.ts +4 -0
  102. package/dist/lib/crabbox/runtimes.js +33 -8
  103. package/dist/lib/crabbox/setup-copy.d.ts +87 -0
  104. package/dist/lib/crabbox/setup-copy.js +127 -0
  105. package/dist/lib/daemon.d.ts +5 -5
  106. package/dist/lib/daemon.js +9 -9
  107. package/dist/lib/drive-sync.js +6 -3
  108. package/dist/lib/event-stream.d.ts +36 -0
  109. package/dist/lib/event-stream.js +68 -0
  110. package/dist/lib/events.d.ts +1 -1
  111. package/dist/lib/exec.d.ts +11 -0
  112. package/dist/lib/exec.js +23 -2
  113. package/dist/lib/export.d.ts +72 -0
  114. package/dist/lib/export.js +269 -0
  115. package/dist/lib/feed-policy.d.ts +1 -1
  116. package/dist/lib/feed-policy.js +14 -9
  117. package/dist/lib/feed-ranking.d.ts +32 -0
  118. package/dist/lib/feed-ranking.js +224 -0
  119. package/dist/lib/feed.d.ts +35 -2
  120. package/dist/lib/feed.js +66 -3
  121. package/dist/lib/fleet/auth-sync.d.ts +53 -0
  122. package/dist/lib/fleet/auth-sync.js +92 -0
  123. package/dist/lib/fleet/remote-login.d.ts +187 -0
  124. package/dist/lib/fleet/remote-login.js +556 -0
  125. package/dist/lib/format.d.ts +30 -3
  126. package/dist/lib/format.js +34 -5
  127. package/dist/lib/fs-atomic.d.ts +7 -2
  128. package/dist/lib/fs-atomic.js +8 -12
  129. package/dist/lib/git.d.ts +16 -0
  130. package/dist/lib/git.js +79 -2
  131. package/dist/lib/heal.js +11 -3
  132. package/dist/lib/hosts/progress.d.ts +28 -10
  133. package/dist/lib/hosts/progress.js +79 -22
  134. package/dist/lib/hosts/remote-cmd.js +4 -0
  135. package/dist/lib/hq/floor.d.ts +87 -0
  136. package/dist/lib/hq/floor.js +226 -0
  137. package/dist/lib/import.d.ts +23 -0
  138. package/dist/lib/import.js +74 -2
  139. package/dist/lib/isolation-boundary-report.d.ts +10 -0
  140. package/dist/lib/isolation-boundary-report.js +35 -0
  141. package/dist/lib/menubar/install-menubar.js +14 -20
  142. package/dist/lib/notify.d.ts +1 -0
  143. package/dist/lib/notify.js +3 -3
  144. package/dist/lib/open-url.d.ts +2 -0
  145. package/dist/lib/open-url.js +19 -0
  146. package/dist/lib/openclaw-keychain.d.ts +56 -0
  147. package/dist/lib/openclaw-keychain.js +236 -0
  148. package/dist/lib/overdue.js +10 -0
  149. package/dist/lib/permissions.d.ts +44 -1
  150. package/dist/lib/permissions.js +284 -7
  151. package/dist/lib/project-launch.d.ts +6 -12
  152. package/dist/lib/project-launch.js +13 -228
  153. package/dist/lib/project-resources.d.ts +7 -0
  154. package/dist/lib/project-resources.js +306 -0
  155. package/dist/lib/pty-client.d.ts +27 -0
  156. package/dist/lib/pty-client.js +136 -10
  157. package/dist/lib/refresh.js +14 -10
  158. package/dist/lib/resource-profiles.d.ts +26 -0
  159. package/dist/lib/resource-profiles.js +157 -0
  160. package/dist/lib/resources/permissions.js +7 -1
  161. package/dist/lib/resources/types.d.ts +1 -1
  162. package/dist/lib/resources.d.ts +1 -1
  163. package/dist/lib/resources.js +32 -3
  164. package/dist/lib/routines.d.ts +11 -0
  165. package/dist/lib/routines.js +56 -15
  166. package/dist/lib/runner.d.ts +1 -0
  167. package/dist/lib/runner.js +66 -5
  168. package/dist/lib/secrets/bundles.d.ts +16 -3
  169. package/dist/lib/secrets/bundles.js +206 -37
  170. package/dist/lib/secrets/icloud-import.d.ts +2 -2
  171. package/dist/lib/secrets/icloud-import.js +9 -6
  172. package/dist/lib/secrets/mcp.js +1 -1
  173. package/dist/lib/secrets/vault-age-helper.d.ts +1 -0
  174. package/dist/lib/secrets/vault-age-helper.js +34 -0
  175. package/dist/lib/secrets/vault.d.ts +49 -0
  176. package/dist/lib/secrets/vault.js +397 -0
  177. package/dist/lib/self-heal/checks/path.js +3 -1
  178. package/dist/lib/self-heal/checks/shadowing.js +10 -2
  179. package/dist/lib/self-heal/checks/shims.js +19 -11
  180. package/dist/lib/session/cloud.d.ts +2 -2
  181. package/dist/lib/session/cloud.js +2 -2
  182. package/dist/lib/session/db.d.ts +4 -0
  183. package/dist/lib/session/db.js +44 -7
  184. package/dist/lib/session/discover.d.ts +2 -0
  185. package/dist/lib/session/discover.js +114 -3
  186. package/dist/lib/session/stream-render.d.ts +3 -0
  187. package/dist/lib/session/stream-render.js +130 -0
  188. package/dist/lib/session/types.d.ts +6 -0
  189. package/dist/lib/share/analytics.d.ts +13 -0
  190. package/dist/lib/share/analytics.js +45 -0
  191. package/dist/lib/share/config.d.ts +19 -5
  192. package/dist/lib/share/config.js +44 -8
  193. package/dist/lib/share/provision.d.ts +58 -6
  194. package/dist/lib/share/provision.js +97 -22
  195. package/dist/lib/share/publish.d.ts +36 -13
  196. package/dist/lib/share/publish.js +55 -8
  197. package/dist/lib/share/worker-template.js +83 -17
  198. package/dist/lib/shims.d.ts +35 -0
  199. package/dist/lib/shims.js +159 -13
  200. package/dist/lib/ssh-exec.d.ts +14 -0
  201. package/dist/lib/ssh-exec.js +57 -0
  202. package/dist/lib/staleness/detectors/hooks.js +25 -1
  203. package/dist/lib/staleness/detectors/permissions.js +66 -0
  204. package/dist/lib/staleness/detectors/workflows.js +20 -0
  205. package/dist/lib/staleness/writers/hooks.js +58 -4
  206. package/dist/lib/staleness/writers/permissions.js +2 -2
  207. package/dist/lib/staleness/writers/skills.js +1 -1
  208. package/dist/lib/staleness/writers/sources.d.ts +10 -1
  209. package/dist/lib/staleness/writers/sources.js +68 -1
  210. package/dist/lib/star-nudge.d.ts +45 -0
  211. package/dist/lib/star-nudge.js +91 -0
  212. package/dist/lib/startup/command-registry.d.ts +7 -1
  213. package/dist/lib/startup/command-registry.js +19 -3
  214. package/dist/lib/state.d.ts +2 -0
  215. package/dist/lib/state.js +17 -4
  216. package/dist/lib/subagents-registry.d.ts +2 -0
  217. package/dist/lib/subagents-registry.js +3 -0
  218. package/dist/lib/teams/parsers.js +214 -6
  219. package/dist/lib/teams/supervisor.d.ts +7 -0
  220. package/dist/lib/teams/supervisor.js +2 -1
  221. package/dist/lib/template.d.ts +1 -1
  222. package/dist/lib/template.js +1 -1
  223. package/dist/lib/triggers/webhook.js +36 -3
  224. package/dist/lib/types.d.ts +84 -0
  225. package/dist/lib/uninstall.js +1 -45
  226. package/dist/lib/version.d.ts +40 -0
  227. package/dist/lib/version.js +94 -16
  228. package/dist/lib/versions.d.ts +24 -0
  229. package/dist/lib/versions.js +269 -78
  230. package/dist/lib/workflows.d.ts +2 -0
  231. package/dist/lib/workflows.js +88 -0
  232. package/package.json +2 -1
  233. package/dist/commands/daemon.d.ts +0 -10
  234. package/dist/commands/daemon.js +0 -121
@@ -433,14 +433,27 @@ export function removePermissionSet(name) {
433
433
  // ============================================================================
434
434
  // Agent-specific converters
435
435
  // ============================================================================
436
+ /**
437
+ * Map a canonical rule to Claude settings.json syntax.
438
+ * Claude's file-permission checks only match Edit(path) rules — an Edit rule
439
+ * covers every file-editing tool (Write, Edit, NotebookEdit) — and Claude
440
+ * warns at startup about unmatched Write(path) rules. Canonical Write(path)
441
+ * stays for other agents' converters; for Claude it is emitted as Edit(path).
442
+ */
443
+ function canonicalToClaudeRule(perm) {
444
+ if (perm.startsWith('Write(')) {
445
+ return perm.replace(/^Write/, 'Edit');
446
+ }
447
+ return perm;
448
+ }
436
449
  /**
437
450
  * Convert canonical permission set to Claude format.
438
451
  * Claude uses: { permissions: { allow: ["Bash(*)", "Read(**)"], deny: [] } }
439
452
  */
440
453
  export function convertToClaudeFormat(set) {
441
454
  const permissions = {
442
- allow: [...set.allow],
443
- deny: set.deny ? [...set.deny] : [],
455
+ allow: [...new Set(set.allow.map(canonicalToClaudeRule))],
456
+ deny: set.deny ? [...new Set(set.deny.map(canonicalToClaudeRule))] : [],
444
457
  };
445
458
  if (set.additionalDirectories?.length) {
446
459
  permissions.additionalDirectories = [...set.additionalDirectories];
@@ -540,6 +553,89 @@ function normalizeBashPattern(pattern) {
540
553
  return pattern.slice(0, -2) + ' *';
541
554
  return pattern;
542
555
  }
556
+ function canonicalBashToCopilotCommandIdentifier(pattern) {
557
+ if (pattern === '*' || pattern === '**')
558
+ return null;
559
+ if (pattern.endsWith(':*'))
560
+ return pattern;
561
+ if (pattern.endsWith(' *'))
562
+ return `${pattern.slice(0, -2)}:*`;
563
+ return pattern;
564
+ }
565
+ function canonicalToCopilotMcpApproval(parsed) {
566
+ const parts = parsed.pattern.split(/[.:/]/).filter(Boolean);
567
+ if (parts.length === 0)
568
+ return null;
569
+ const [serverName, toolName] = parts;
570
+ return { kind: 'mcp', serverName, toolName: toolName ?? null };
571
+ }
572
+ function canonicalToCopilotApproval(permission) {
573
+ const parsed = parseCanonicalPattern(permission);
574
+ if (!parsed)
575
+ return null;
576
+ if (parsed.tool === 'bash') {
577
+ const identifier = canonicalBashToCopilotCommandIdentifier(parsed.pattern);
578
+ return identifier ? { kind: 'commands', commandIdentifiers: [identifier] } : null;
579
+ }
580
+ if ((parsed.tool === 'write' || parsed.tool === 'edit') && (parsed.pattern === '*' || parsed.pattern === '**')) {
581
+ return { kind: 'write' };
582
+ }
583
+ if (parsed.tool === 'read' && (parsed.pattern === '*' || parsed.pattern === '**')) {
584
+ return { kind: 'read' };
585
+ }
586
+ if (parsed.tool === 'mcp') {
587
+ return canonicalToCopilotMcpApproval(parsed);
588
+ }
589
+ return null;
590
+ }
591
+ function mergeCopilotApprovals(existing, incoming) {
592
+ const byKey = new Map();
593
+ const add = (approval) => {
594
+ if (approval.kind === 'commands') {
595
+ const current = byKey.get('commands');
596
+ const commandIdentifiers = new Set([...(current?.commandIdentifiers ?? []), ...approval.commandIdentifiers]);
597
+ byKey.set('commands', { kind: 'commands', commandIdentifiers: Array.from(commandIdentifiers).sort() });
598
+ return;
599
+ }
600
+ byKey.set(JSON.stringify(approval), approval);
601
+ };
602
+ for (const approval of existing) {
603
+ if (!approval || typeof approval !== 'object' || Array.isArray(approval))
604
+ continue;
605
+ const record = approval;
606
+ if (record.kind === 'commands' && Array.isArray(record.commandIdentifiers)) {
607
+ add({ kind: 'commands', commandIdentifiers: record.commandIdentifiers.filter((v) => typeof v === 'string') });
608
+ }
609
+ else if (record.kind === 'read' || record.kind === 'write') {
610
+ add({ kind: record.kind });
611
+ }
612
+ else if (record.kind === 'mcp' && typeof record.serverName === 'string' && (typeof record.toolName === 'string' || record.toolName === null)) {
613
+ add({ kind: 'mcp', serverName: record.serverName, toolName: record.toolName });
614
+ }
615
+ }
616
+ for (const approval of incoming)
617
+ add(approval);
618
+ return Array.from(byKey.values());
619
+ }
620
+ export function convertToCopilotFormat(set, location) {
621
+ const approvals = [];
622
+ for (const permission of set.allow) {
623
+ const approval = canonicalToCopilotApproval(permission);
624
+ if (approval)
625
+ approvals.push(approval);
626
+ }
627
+ const allowedDirectories = (set.additionalDirectories ?? [])
628
+ .filter((dir) => dir.trim().length > 0)
629
+ .map((dir) => path.isAbsolute(dir) ? dir : path.resolve(location, dir));
630
+ return {
631
+ locations: {
632
+ [location]: {
633
+ ...(approvals.length > 0 ? { tool_approvals: mergeCopilotApprovals([], approvals) } : {}),
634
+ ...(allowedDirectories.length > 0 ? { allowed_directories: Array.from(new Set(allowedDirectories)).sort() } : {}),
635
+ },
636
+ },
637
+ };
638
+ }
543
639
  /** Convert canonical Bash rules into Droid's command arrays. */
544
640
  export function convertToDroidFormat(set) {
545
641
  const commands = (permissions) => {
@@ -614,6 +710,83 @@ export function convertToOpenClawFormat(set) {
614
710
  deny: map(set.deny ?? []),
615
711
  };
616
712
  }
713
+ const FORGE_OPERATION_BY_CANONICAL = {
714
+ bash: 'command',
715
+ read: 'read',
716
+ grep: 'read',
717
+ glob: 'read',
718
+ write: 'write',
719
+ edit: 'write',
720
+ notebookedit: 'write',
721
+ webfetch: 'url',
722
+ websearch: 'url',
723
+ };
724
+ function canonicalToForgePolicy(perm, permission) {
725
+ if (BLANKET_BASH_FORMS.has(perm)) {
726
+ return { permission, rule: { command: '*' } };
727
+ }
728
+ const parsed = parseCanonicalPattern(perm);
729
+ if (!parsed)
730
+ return null;
731
+ const operation = FORGE_OPERATION_BY_CANONICAL[parsed.tool];
732
+ if (!operation)
733
+ return null;
734
+ const pattern = parsed.tool === 'bash'
735
+ ? normalizeBashPattern(parsed.pattern)
736
+ : (parsed.tool === 'webfetch' || parsed.tool === 'websearch') && parsed.pattern.startsWith('domain:')
737
+ ? `${parsed.pattern.slice('domain:'.length)}*`
738
+ : parsed.pattern === '**'
739
+ ? '**/*'
740
+ : parsed.pattern;
741
+ return { permission, rule: { [operation]: pattern } };
742
+ }
743
+ /**
744
+ * Convert canonical permissions to ForgeCode's permissions.yaml policy list.
745
+ *
746
+ * ForgeCode gates built-in tools by operation family (`read`, `write`,
747
+ * `command`, `url`) plus glob patterns, optionally scoped by `dir`. The policy
748
+ * file is ignored unless `.forge.toml` has `restricted = true`, and MCP tools
749
+ * bypass permissions.yaml entirely; agents-cli therefore maps only canonical
750
+ * built-in allow/deny rules and does not try to express per-named-MCP-tool
751
+ * grants.
752
+ */
753
+ export function convertToForgeFormat(set) {
754
+ const policies = [];
755
+ const seen = new Set();
756
+ const add = (policy) => {
757
+ if (!policy)
758
+ return;
759
+ const key = `${policy.permission}|${JSON.stringify(policy.rule)}`;
760
+ if (seen.has(key))
761
+ return;
762
+ seen.add(key);
763
+ policies.push(policy);
764
+ };
765
+ for (const perm of set.allow)
766
+ add(canonicalToForgePolicy(perm, 'allow'));
767
+ for (const perm of set.deny ?? [])
768
+ add(canonicalToForgePolicy(perm, 'deny'));
769
+ return { policies };
770
+ }
771
+ export function convertToHermesFormat(set) {
772
+ const commands = (permissions) => {
773
+ const out = new Set();
774
+ for (const perm of permissions) {
775
+ if (BLANKET_BASH_FORMS.has(perm)) {
776
+ out.add('*');
777
+ continue;
778
+ }
779
+ const parsed = parseCanonicalPattern(perm);
780
+ if (parsed?.tool === 'bash')
781
+ out.add(normalizeBashPattern(parsed.pattern));
782
+ }
783
+ return Array.from(out).sort();
784
+ };
785
+ return {
786
+ command_allowlist: commands(set.allow),
787
+ approvals: { deny: commands(set.deny ?? []) },
788
+ };
789
+ }
617
790
  /**
618
791
  * Convert canonical permission set to Antigravity format.
619
792
  * Antigravity reads ~/.gemini/antigravity-cli/settings.json with
@@ -1144,8 +1317,9 @@ export function applyClaudePermissions(set, scope = 'user', cwd, merge = true) {
1144
1317
  const newPermissions = convertToClaudeFormat(set);
1145
1318
  if (merge && config.permissions) {
1146
1319
  const existing = config.permissions;
1147
- const mergedAllow = new Set([...(existing.allow || []), ...newPermissions.permissions.allow]);
1148
- const mergedDeny = new Set([...(existing.deny || []), ...newPermissions.permissions.deny]);
1320
+ // Rewrite stale Write(path) rules already installed in settings.json too.
1321
+ const mergedAllow = new Set([...(existing.allow || []).map(canonicalToClaudeRule), ...newPermissions.permissions.allow]);
1322
+ const mergedDeny = new Set([...(existing.deny || []).map(canonicalToClaudeRule), ...newPermissions.permissions.deny]);
1149
1323
  config.permissions = {
1150
1324
  allow: [...mergedAllow],
1151
1325
  deny: [...mergedDeny],
@@ -1292,7 +1466,7 @@ function applyPermissionsToAgent(agentId, set, scope = 'user', cwd, merge = true
1292
1466
  * Apply a permission set to a specific version's home directory.
1293
1467
  * This writes to {versionHome}/.{agent}/settings.json (or equivalent).
1294
1468
  */
1295
- export function applyPermissionsToVersion(agentId, set, versionHome, merge = true) {
1469
+ export function applyPermissionsToVersion(agentId, set, versionHome, merge = true, cwd) {
1296
1470
  const configDir = path.join(versionHome, agentConfigDirName(agentId));
1297
1471
  try {
1298
1472
  fs.mkdirSync(configDir, { recursive: true });
@@ -1305,8 +1479,9 @@ export function applyPermissionsToVersion(agentId, set, versionHome, merge = tru
1305
1479
  const newPermissions = convertToClaudeFormat(set);
1306
1480
  if (merge && config.permissions) {
1307
1481
  const existing = config.permissions;
1308
- const mergedAllow = new Set([...(existing.allow || []), ...newPermissions.permissions.allow]);
1309
- const mergedDeny = new Set([...(existing.deny || []), ...newPermissions.permissions.deny]);
1482
+ // Rewrite stale Write(path) rules already installed in settings.json too.
1483
+ const mergedAllow = new Set([...(existing.allow || []).map(canonicalToClaudeRule), ...newPermissions.permissions.allow]);
1484
+ const mergedDeny = new Set([...(existing.deny || []).map(canonicalToClaudeRule), ...newPermissions.permissions.deny]);
1310
1485
  const mergedDirs = new Set([...(existing.additionalDirectories || []), ...(newPermissions.permissions.additionalDirectories || [])]);
1311
1486
  const perms = {
1312
1487
  allow: [...mergedAllow],
@@ -1593,6 +1768,50 @@ export function applyPermissionsToVersion(agentId, set, versionHome, merge = tru
1593
1768
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
1594
1769
  return { success: true };
1595
1770
  }
1771
+ if (agentId === 'copilot') {
1772
+ const configPath = path.join(versionHome, '.copilot', 'permissions-config.json');
1773
+ let config = { locations: {} };
1774
+ if (fs.existsSync(configPath)) {
1775
+ config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
1776
+ }
1777
+ const location = path.resolve(cwd ?? process.cwd());
1778
+ const converted = convertToCopilotFormat(set, location);
1779
+ const incoming = converted.locations[location]?.tool_approvals ?? [];
1780
+ const incomingDirectories = converted.locations[location]?.allowed_directories ?? [];
1781
+ if (merge && incoming.length === 0 && incomingDirectories.length === 0)
1782
+ return { success: true };
1783
+ const locations = (typeof config.locations === 'object' && config.locations !== null && !Array.isArray(config.locations))
1784
+ ? config.locations
1785
+ : {};
1786
+ const existingLocation = (typeof locations[location] === 'object' && locations[location] !== null && !Array.isArray(locations[location]))
1787
+ ? locations[location]
1788
+ : {};
1789
+ const existingApprovals = Array.isArray(existingLocation.tool_approvals) ? existingLocation.tool_approvals : [];
1790
+ const nextApprovals = merge ? mergeCopilotApprovals(existingApprovals, incoming) : incoming;
1791
+ const existingDirectories = Array.isArray(existingLocation.allowed_directories)
1792
+ ? existingLocation.allowed_directories.filter((v) => typeof v === 'string')
1793
+ : [];
1794
+ const nextDirectories = merge
1795
+ ? Array.from(new Set([...existingDirectories, ...incomingDirectories])).sort()
1796
+ : incomingDirectories;
1797
+ const nextLocation = { ...existingLocation };
1798
+ if (nextApprovals.length > 0)
1799
+ nextLocation.tool_approvals = nextApprovals;
1800
+ else
1801
+ delete nextLocation.tool_approvals;
1802
+ if (nextDirectories.length > 0)
1803
+ nextLocation.allowed_directories = nextDirectories;
1804
+ else
1805
+ delete nextLocation.allowed_directories;
1806
+ if (Object.keys(nextLocation).length > 0)
1807
+ locations[location] = nextLocation;
1808
+ else
1809
+ delete locations[location];
1810
+ config.locations = locations;
1811
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
1812
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
1813
+ return { success: true };
1814
+ }
1596
1815
  if (agentId === 'kiro') {
1597
1816
  const permissionsPath = path.join(versionHome, '.kiro', 'settings', 'permissions.yaml');
1598
1817
  let config = {};
@@ -1669,6 +1888,64 @@ export function applyPermissionsToVersion(agentId, set, versionHome, merge = tru
1669
1888
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
1670
1889
  return { success: true };
1671
1890
  }
1891
+ if (agentId === 'forge') {
1892
+ const permissionsPath = path.join(versionHome, '.forge', 'permissions.yaml');
1893
+ let config = {};
1894
+ if (fs.existsSync(permissionsPath)) {
1895
+ const parsed = yaml.parse(fs.readFileSync(permissionsPath, 'utf-8'));
1896
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
1897
+ config = parsed;
1898
+ }
1899
+ }
1900
+ const newPolicies = convertToForgeFormat(set).policies;
1901
+ if (merge) {
1902
+ const existingPolicies = Array.isArray(config.policies) ? config.policies : [];
1903
+ const seen = new Set();
1904
+ config.policies = [...existingPolicies, ...newPolicies].filter((policy) => {
1905
+ if (!policy || typeof policy !== 'object' || Array.isArray(policy))
1906
+ return false;
1907
+ const key = JSON.stringify(policy);
1908
+ if (seen.has(key))
1909
+ return false;
1910
+ seen.add(key);
1911
+ return true;
1912
+ });
1913
+ }
1914
+ else {
1915
+ config.policies = newPolicies;
1916
+ }
1917
+ fs.mkdirSync(path.dirname(permissionsPath), { recursive: true });
1918
+ fs.writeFileSync(permissionsPath, yaml.stringify(config), 'utf-8');
1919
+ return { success: true };
1920
+ }
1921
+ if (agentId === 'hermes') {
1922
+ const configPath = path.join(versionHome, '.hermes', 'config.yaml');
1923
+ let config = {};
1924
+ if (fs.existsSync(configPath)) {
1925
+ const parsed = yaml.parse(fs.readFileSync(configPath, 'utf-8'));
1926
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
1927
+ config = parsed;
1928
+ }
1929
+ }
1930
+ const converted = convertToHermesFormat(set);
1931
+ const approvals = (typeof config.approvals === 'object' && config.approvals !== null && !Array.isArray(config.approvals))
1932
+ ? config.approvals
1933
+ : {};
1934
+ if (merge) {
1935
+ const existingAllow = Array.isArray(config.command_allowlist) ? config.command_allowlist : [];
1936
+ const existingDeny = Array.isArray(approvals.deny) ? approvals.deny : [];
1937
+ config.command_allowlist = Array.from(new Set([...existingAllow, ...converted.command_allowlist])).sort();
1938
+ approvals.deny = Array.from(new Set([...existingDeny, ...converted.approvals.deny])).sort();
1939
+ }
1940
+ else {
1941
+ config.command_allowlist = converted.command_allowlist;
1942
+ approvals.deny = converted.approvals.deny;
1943
+ }
1944
+ config.approvals = approvals;
1945
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
1946
+ fs.writeFileSync(configPath, yaml.stringify(config), 'utf-8');
1947
+ return { success: true };
1948
+ }
1672
1949
  return { success: false, error: `Agent '${agentId}' does not support permissions` };
1673
1950
  }
1674
1951
  catch (err) {
@@ -8,18 +8,12 @@
8
8
  * (+ per-agent symlinks). Delegates to compileRulesForProject, which is
9
9
  * the same helper management-side `agents sync` uses.
10
10
  *
11
- * 2. Mirror project resources from `<cwd>/.agents/{subagents,commands,skills}`
12
- * into the agent's workspace-local discovery dirs (`<cwd>/.claude/agents/`,
13
- * `<cwd>/.claude/commands/`, `<cwd>/.claude/skills/`). File-level symlinks
14
- * so edits in the source dir are live without re-sync. Skips any dest
15
- * entry that already exists and isn't one of our symlinks (don't-clobber).
16
- * v1: claude-only. Other agents have varying workspace conventions (amp
17
- * uses `~/.config/amp`, antigravity uses `~/.gemini/antigravity-cli`,
18
- * codex/gemini/cursor lack subagent support entirely) — they're follow-up
19
- * material. NOTE: `.mcp.json` is intentionally NOT auto-symlinked from
20
- * the launch path — that's a supply-chain surface (cloning a hostile
21
- * repo would auto-register an attacker MCP server). Belongs to full
22
- * `agents sync` with explicit opt-in, not the hot path.
11
+ * 2. Copy project resources from `<cwd>/.agents/{commands,skills,subagents,workflows}`
12
+ * into the agent's workspace-local discovery dir (`<cwd>/.claude/`,
13
+ * `<cwd>/.codex/`, etc.) with an ownership manifest at
14
+ * `<cwd>/.{agent}/.agents-managed.json`. The manifest is the only clobber
15
+ * authority: paths it lists are removed and refreshed, pre-existing paths it
16
+ * does not list are skipped as user-owned.
23
17
  *
24
18
  * 3. Synthesize four scope-grouped plugin marketplaces under the version's
25
19
  * `<versionHome>/.{agent}/plugins/marketplaces/` (for plugin-capable agents):
@@ -8,18 +8,12 @@
8
8
  * (+ per-agent symlinks). Delegates to compileRulesForProject, which is
9
9
  * the same helper management-side `agents sync` uses.
10
10
  *
11
- * 2. Mirror project resources from `<cwd>/.agents/{subagents,commands,skills}`
12
- * into the agent's workspace-local discovery dirs (`<cwd>/.claude/agents/`,
13
- * `<cwd>/.claude/commands/`, `<cwd>/.claude/skills/`). File-level symlinks
14
- * so edits in the source dir are live without re-sync. Skips any dest
15
- * entry that already exists and isn't one of our symlinks (don't-clobber).
16
- * v1: claude-only. Other agents have varying workspace conventions (amp
17
- * uses `~/.config/amp`, antigravity uses `~/.gemini/antigravity-cli`,
18
- * codex/gemini/cursor lack subagent support entirely) — they're follow-up
19
- * material. NOTE: `.mcp.json` is intentionally NOT auto-symlinked from
20
- * the launch path — that's a supply-chain surface (cloning a hostile
21
- * repo would auto-register an attacker MCP server). Belongs to full
22
- * `agents sync` with explicit opt-in, not the hot path.
11
+ * 2. Copy project resources from `<cwd>/.agents/{commands,skills,subagents,workflows}`
12
+ * into the agent's workspace-local discovery dir (`<cwd>/.claude/`,
13
+ * `<cwd>/.codex/`, etc.) with an ownership manifest at
14
+ * `<cwd>/.{agent}/.agents-managed.json`. The manifest is the only clobber
15
+ * authority: paths it lists are removed and refreshed, pre-existing paths it
16
+ * does not list are skipped as user-owned.
23
17
  *
24
18
  * 3. Synthesize four scope-grouped plugin marketplaces under the version's
25
19
  * `<versionHome>/.{agent}/plugins/marketplaces/` (for plugin-capable agents):
@@ -48,8 +42,8 @@ import { supports } from './capabilities.js';
48
42
  import { getEnabledExtraRepos, getExtraPluginsDir, getPluginsDir, getProjectAgentsDir, getProjectPluginsDir, getSystemPluginsDir, } from './state.js';
49
43
  import { getVersionHomePath } from './versions.js';
50
44
  import { toPortableKey } from './platform/index.js';
51
- import { transformSubagentForClaude } from './subagents.js';
52
45
  import { compileRulesForProject } from './rules/compile.js';
46
+ import { syncProjectResourcesToAgent } from './project-resources.js';
53
47
  import { discoverPluginsInDir, hasPluginExecSurfaces, inspectPluginCapabilities } from './plugins.js';
54
48
  import { MARKETPLACE_NAME, PROJECT_MARKETPLACE_NAME, SYSTEM_MARKETPLACE_NAME, addPluginToSettings, copyPluginToMarketplace, marketplaceNameFor, marketplaceRoot, pluginInstallDir, registerMarketplace, removePluginFromSettings, syncMarketplaceManifest, } from './plugin-marketplace.js';
55
49
  /**
@@ -77,9 +71,12 @@ export function runLaunchSync(opts) {
77
71
  // Don't fail launch on a malformed project rules.yaml.
78
72
  }
79
73
  // Step 2: workspace resource mirror
80
- const mirror = mirrorWorkspaceResources(opts.cwd, opts.agent);
81
- result.workspaceLinks = mirror.links;
82
- result.workspaceSkipped = mirror.skipped;
74
+ const projectAgentsDir = getProjectAgentsDir(opts.cwd);
75
+ if (projectAgentsDir) {
76
+ const projectResources = syncProjectResourcesToAgent(opts.agent, opts.version, projectAgentsDir);
77
+ result.workspaceLinks = projectResources.synced.length;
78
+ result.workspaceSkipped = projectResources.skipped;
79
+ }
83
80
  // Step 3: scoped plugin marketplaces
84
81
  result.marketplaces = synthesizeScopedMarketplaces(opts.agent, opts.version, opts.cwd);
85
82
  // Touch the shim's skip-fast sentinel. Best-effort — if this fails the
@@ -117,218 +114,6 @@ function touchLaunchSentinel(agent, version, cwd) {
117
114
  // best-effort
118
115
  }
119
116
  }
120
- const CLAUDE_MIRROR_PLANS = [
121
- { srcSubdir: 'subagents', destSubdir: 'agents', mode: 'subagent-write' },
122
- { srcSubdir: 'commands', destSubdir: 'commands', mode: 'file-symlink' },
123
- { srcSubdir: 'skills', destSubdir: 'skills', mode: 'dir-symlink' },
124
- ];
125
- /**
126
- * Marker prepended-as-trailing-comment to every subagent file WE generate.
127
- * It's an HTML comment — invisible to the markdown the agent reads — placed on
128
- * the last line so it never disturbs the leading `---` frontmatter block.
129
- *
130
- * Ownership rule (the one don't-clobber decision for written, non-symlink
131
- * files): we only overwrite a `.claude/agents/<name>.md` whose content carries
132
- * this marker. A user-authored file (no marker) or a symlink at the dest is
133
- * left untouched. A marker beats an mtime/sidecar sentinel because it travels
134
- * with the file across copies and git, and needs no out-of-band state.
135
- */
136
- const GENERATED_SUBAGENT_MARKER = '<!-- agents-cli:generated-subagent';
137
- function mirrorWorkspaceResources(cwd, agent) {
138
- // v1: claude-only. Other agents have workspace conventions we haven't
139
- // mapped (amp: ~/.config/amp; antigravity: ~/.gemini/antigravity-cli;
140
- // codex/gemini/cursor: no subagent support per capabilities). Adding them
141
- // requires per-agent workspaceDirName + capability gates — follow-up.
142
- if (agent !== 'claude')
143
- return { links: 0, skipped: [] };
144
- const projectAgentsDir = getProjectAgentsDir(cwd);
145
- if (!projectAgentsDir)
146
- return { links: 0, skipped: [] };
147
- const agentWorkspaceDir = path.join(cwd, '.claude');
148
- const projectAgentsResolved = (() => {
149
- try {
150
- return fs.realpathSync(projectAgentsDir);
151
- }
152
- catch {
153
- return path.resolve(projectAgentsDir);
154
- }
155
- })();
156
- let links = 0;
157
- const skipped = [];
158
- // Mirror subagents / commands / skills. mcp.json is intentionally excluded
159
- // — see header doc, it's a supply-chain surface.
160
- for (const plan of CLAUDE_MIRROR_PLANS) {
161
- const srcDir = path.join(projectAgentsDir, plan.srcSubdir);
162
- if (!fs.existsSync(srcDir))
163
- continue;
164
- const destDir = path.join(agentWorkspaceDir, plan.destSubdir);
165
- fs.mkdirSync(destDir, { recursive: true });
166
- // Subagents flatten N source files into one written .md — not a symlink.
167
- if (plan.mode === 'subagent-write') {
168
- const r = writeProjectSubagents(srcDir, destDir, cwd);
169
- links += r.links;
170
- skipped.push(...r.skipped);
171
- continue;
172
- }
173
- const entries = fs.readdirSync(srcDir, { withFileTypes: true });
174
- for (const entry of entries) {
175
- if (entry.name.startsWith('.'))
176
- continue;
177
- if (plan.mode === 'dir-symlink' && !entry.isDirectory())
178
- continue;
179
- if (plan.mode === 'file-symlink' && !entry.isFile() && !entry.isSymbolicLink())
180
- continue;
181
- const srcPath = path.join(srcDir, entry.name);
182
- const destPath = path.join(destDir, entry.name);
183
- if (replaceWithSymlinkIfOwned(srcPath, destPath, projectAgentsResolved)) {
184
- links += 1;
185
- }
186
- else {
187
- skipped.push(path.relative(cwd, destPath));
188
- }
189
- }
190
- }
191
- return { links, skipped };
192
- }
193
- /**
194
- * Mirror project subagents into `<cwd>/.claude/agents/`. The canonical source
195
- * shape is a DIRECTORY containing AGENT.md (e.g. `.agents/subagents/probe/AGENT.md`)
196
- * — confirmed by the detector (versions.ts) and lister (subagents.ts). Each
197
- * such directory is flattened via transformSubagentForClaude (the exact writer
198
- * the version-home sync uses) into a single `<name>.md`, then written under an
199
- * ownership marker so a re-launch refreshes our file but never clobbers a
200
- * user-authored one.
201
- *
202
- * Returns the same {links, skipped} shape the symlink path reports, so the
203
- * caller's accounting is uniform across resource kinds.
204
- */
205
- function writeProjectSubagents(srcDir, destDir, cwd) {
206
- let links = 0;
207
- const skipped = [];
208
- for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
209
- if (entry.name.startsWith('.'))
210
- continue;
211
- if (!entry.isDirectory())
212
- continue;
213
- const subagentDir = path.join(srcDir, entry.name);
214
- if (!fs.existsSync(path.join(subagentDir, 'AGENT.md')))
215
- continue;
216
- const destPath = path.join(destDir, `${entry.name}.md`);
217
- if (writeSubagentIfOwned(subagentDir, destPath)) {
218
- links += 1;
219
- }
220
- else {
221
- skipped.push(path.relative(cwd, destPath));
222
- }
223
- }
224
- return { links, skipped };
225
- }
226
- /**
227
- * Write a flattened subagent file at `destPath`, refusing to clobber user state.
228
- *
229
- * - dest missing → write fresh.
230
- * - dest is our generation → overwrite (refresh; carries GENERATED_SUBAGENT_MARKER).
231
- * - dest is a symlink / any
232
- * non-regular file → SKIP (user state we don't own).
233
- * - dest is a regular file
234
- * without our marker → SKIP (hand-authored .claude/agents/<name>.md).
235
- *
236
- * Returns true when our file is present (written now or already current),
237
- * false when we left a user-owned dest alone.
238
- */
239
- function writeSubagentIfOwned(subagentDir, destPath) {
240
- let existing = null;
241
- let destLstat = null;
242
- try {
243
- destLstat = fs.lstatSync(destPath);
244
- }
245
- catch { /* missing — write fresh */ }
246
- if (destLstat) {
247
- if (!destLstat.isFile())
248
- return false; // symlink/dir/etc. — user state
249
- try {
250
- existing = fs.readFileSync(destPath, 'utf-8');
251
- }
252
- catch {
253
- return false;
254
- }
255
- if (!existing.includes(GENERATED_SUBAGENT_MARKER))
256
- return false; // hand-authored
257
- }
258
- let body;
259
- try {
260
- body = transformSubagentForClaude(subagentDir);
261
- }
262
- catch {
263
- return false; // malformed AGENT.md — don't write a broken file
264
- }
265
- const content = `${body}\n\n${GENERATED_SUBAGENT_MARKER} — edit .agents/subagents/${path.basename(subagentDir)}/ instead -->\n`;
266
- // Skip-fast: identical content already on disk → no write (keeps mtime stable).
267
- if (existing === content)
268
- return true;
269
- try {
270
- fs.mkdirSync(path.dirname(destPath), { recursive: true });
271
- fs.writeFileSync(destPath, content);
272
- }
273
- catch {
274
- return false;
275
- }
276
- return true;
277
- }
278
- /**
279
- * Create or refresh a symlink at `destPath` pointing at `srcPath`. Returns
280
- * true if we wrote (or already had) the link, false if we skipped because
281
- * the destination is user-owned (regular file, directory, or symlink pointing
282
- * outside the project's `.agents/` tree, or a dangling symlink — treated as
283
- * user state we don't yet understand).
284
- *
285
- * Skip-fast: if the destination is already a symlink resolving to the
286
- * project-agents tree AND its target matches srcPath, no write happens.
287
- */
288
- function replaceWithSymlinkIfOwned(srcPath, destPath, projectAgentsResolved) {
289
- let destLstat = null;
290
- try {
291
- destLstat = fs.lstatSync(destPath);
292
- }
293
- catch { /* missing — write fresh */ }
294
- if (destLstat) {
295
- if (!destLstat.isSymbolicLink()) {
296
- return false;
297
- }
298
- let destTargetReal = null;
299
- try {
300
- destTargetReal = fs.realpathSync(destPath);
301
- }
302
- catch { /* dangling */ }
303
- // Dangling symlink → user-owned in-progress state; do not clobber.
304
- if (destTargetReal === null) {
305
- return false;
306
- }
307
- if (destTargetReal !== projectAgentsResolved && !destTargetReal.startsWith(projectAgentsResolved + path.sep)) {
308
- return false;
309
- }
310
- let srcReal = null;
311
- try {
312
- srcReal = fs.realpathSync(srcPath);
313
- }
314
- catch { /* src vanished mid-launch */ }
315
- if (srcReal !== null && destTargetReal === srcReal) {
316
- return true; // already correct
317
- }
318
- try {
319
- fs.unlinkSync(destPath);
320
- }
321
- catch { /* ignore */ }
322
- }
323
- try {
324
- fs.mkdirSync(path.dirname(destPath), { recursive: true });
325
- fs.symlinkSync(srcPath, destPath);
326
- }
327
- catch {
328
- return false;
329
- }
330
- return true;
331
- }
332
117
  function makeScope(spec, pluginsDir, autoEnableExecSurfaces, precedence) {
333
118
  return { spec, marketplaceName: marketplaceNameFor(spec), pluginsDir, autoEnableExecSurfaces, precedence };
334
119
  }
@@ -0,0 +1,7 @@
1
+ import type { AgentId } from './types.js';
2
+ export interface ProjectResourceSyncResult {
3
+ synced: string[];
4
+ skipped: string[];
5
+ }
6
+ export declare function projectAgentRoot(projectRoot: string, agent: AgentId): string;
7
+ export declare function syncProjectResourcesToAgent(agent: AgentId, version: string, projectAgentsDir: string): ProjectResourceSyncResult;