@bazaar.ai/mcp-human-agents 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (259) hide show
  1. package/.env.example +15 -0
  2. package/README.md +178 -0
  3. package/dist/mcp-server/src/bin.d.ts +3 -0
  4. package/dist/mcp-server/src/bin.d.ts.map +1 -0
  5. package/dist/mcp-server/src/bin.js +10 -0
  6. package/dist/mcp-server/src/bin.js.map +1 -0
  7. package/dist/mcp-server/src/cli/setup.d.ts +2 -0
  8. package/dist/mcp-server/src/cli/setup.d.ts.map +1 -0
  9. package/dist/mcp-server/src/cli/setup.js +274 -0
  10. package/dist/mcp-server/src/cli/setup.js.map +1 -0
  11. package/dist/mcp-server/src/config/defaults.d.ts +16 -0
  12. package/dist/mcp-server/src/config/defaults.d.ts.map +1 -0
  13. package/dist/mcp-server/src/config/defaults.js +19 -0
  14. package/dist/mcp-server/src/config/defaults.js.map +1 -0
  15. package/dist/mcp-server/src/config/env.d.ts +73 -0
  16. package/dist/mcp-server/src/config/env.d.ts.map +1 -0
  17. package/dist/mcp-server/src/config/env.js +72 -0
  18. package/dist/mcp-server/src/config/env.js.map +1 -0
  19. package/dist/mcp-server/src/config/index.d.ts +3 -0
  20. package/dist/mcp-server/src/config/index.d.ts.map +1 -0
  21. package/dist/mcp-server/src/config/index.js +22 -0
  22. package/dist/mcp-server/src/config/index.js.map +1 -0
  23. package/dist/mcp-server/src/context/generator.d.ts +16 -0
  24. package/dist/mcp-server/src/context/generator.d.ts.map +1 -0
  25. package/dist/mcp-server/src/context/generator.js +61 -0
  26. package/dist/mcp-server/src/context/generator.js.map +1 -0
  27. package/dist/mcp-server/src/context/index.d.ts +3 -0
  28. package/dist/mcp-server/src/context/index.d.ts.map +1 -0
  29. package/dist/mcp-server/src/context/index.js +10 -0
  30. package/dist/mcp-server/src/context/index.js.map +1 -0
  31. package/dist/mcp-server/src/context/templates.d.ts +8 -0
  32. package/dist/mcp-server/src/context/templates.d.ts.map +1 -0
  33. package/dist/mcp-server/src/context/templates.js +41 -0
  34. package/dist/mcp-server/src/context/templates.js.map +1 -0
  35. package/dist/mcp-server/src/git/branch.d.ts +13 -0
  36. package/dist/mcp-server/src/git/branch.d.ts.map +1 -0
  37. package/dist/mcp-server/src/git/branch.js +49 -0
  38. package/dist/mcp-server/src/git/branch.js.map +1 -0
  39. package/dist/mcp-server/src/git/diff.d.ts +10 -0
  40. package/dist/mcp-server/src/git/diff.d.ts.map +1 -0
  41. package/dist/mcp-server/src/git/diff.js +39 -0
  42. package/dist/mcp-server/src/git/diff.js.map +1 -0
  43. package/dist/mcp-server/src/git/index.d.ts +5 -0
  44. package/dist/mcp-server/src/git/index.d.ts.map +1 -0
  45. package/dist/mcp-server/src/git/index.js +16 -0
  46. package/dist/mcp-server/src/git/index.js.map +1 -0
  47. package/dist/mcp-server/src/git/merge.d.ts +6 -0
  48. package/dist/mcp-server/src/git/merge.d.ts.map +1 -0
  49. package/dist/mcp-server/src/git/merge.js +30 -0
  50. package/dist/mcp-server/src/git/merge.js.map +1 -0
  51. package/dist/mcp-server/src/git/worktree.d.ts +11 -0
  52. package/dist/mcp-server/src/git/worktree.d.ts.map +1 -0
  53. package/dist/mcp-server/src/git/worktree.js +38 -0
  54. package/dist/mcp-server/src/git/worktree.js.map +1 -0
  55. package/dist/mcp-server/src/http-wrapper.d.ts +6 -0
  56. package/dist/mcp-server/src/http-wrapper.d.ts.map +1 -0
  57. package/dist/mcp-server/src/http-wrapper.js +85 -0
  58. package/dist/mcp-server/src/http-wrapper.js.map +1 -0
  59. package/dist/mcp-server/src/index.d.ts +2 -0
  60. package/dist/mcp-server/src/index.d.ts.map +1 -0
  61. package/dist/mcp-server/src/index.js +28 -0
  62. package/dist/mcp-server/src/index.js.map +1 -0
  63. package/dist/mcp-server/src/platform-client/client.d.ts +17 -0
  64. package/dist/mcp-server/src/platform-client/client.d.ts.map +1 -0
  65. package/dist/mcp-server/src/platform-client/client.js +68 -0
  66. package/dist/mcp-server/src/platform-client/client.js.map +1 -0
  67. package/dist/mcp-server/src/platform-client/index.d.ts +5 -0
  68. package/dist/mcp-server/src/platform-client/index.d.ts.map +1 -0
  69. package/dist/mcp-server/src/platform-client/index.js +10 -0
  70. package/dist/mcp-server/src/platform-client/index.js.map +1 -0
  71. package/dist/mcp-server/src/platform-client/mock-client.d.ts +28 -0
  72. package/dist/mcp-server/src/platform-client/mock-client.d.ts.map +1 -0
  73. package/dist/mcp-server/src/platform-client/mock-client.js +75 -0
  74. package/dist/mcp-server/src/platform-client/mock-client.js.map +1 -0
  75. package/dist/mcp-server/src/platform-client/polling.d.ts +9 -0
  76. package/dist/mcp-server/src/platform-client/polling.d.ts.map +1 -0
  77. package/dist/mcp-server/src/platform-client/polling.js +40 -0
  78. package/dist/mcp-server/src/platform-client/polling.js.map +1 -0
  79. package/dist/mcp-server/src/platform-client/types.d.ts +2 -0
  80. package/dist/mcp-server/src/platform-client/types.d.ts.map +1 -0
  81. package/dist/mcp-server/src/platform-client/types.js +3 -0
  82. package/dist/mcp-server/src/platform-client/types.js.map +1 -0
  83. package/dist/mcp-server/src/provisioning/authorized-keys.d.ts +14 -0
  84. package/dist/mcp-server/src/provisioning/authorized-keys.d.ts.map +1 -0
  85. package/dist/mcp-server/src/provisioning/authorized-keys.js +48 -0
  86. package/dist/mcp-server/src/provisioning/authorized-keys.js.map +1 -0
  87. package/dist/mcp-server/src/provisioning/cleanup.d.ts +19 -0
  88. package/dist/mcp-server/src/provisioning/cleanup.d.ts.map +1 -0
  89. package/dist/mcp-server/src/provisioning/cleanup.js +96 -0
  90. package/dist/mcp-server/src/provisioning/cleanup.js.map +1 -0
  91. package/dist/mcp-server/src/provisioning/index.d.ts +6 -0
  92. package/dist/mcp-server/src/provisioning/index.d.ts.map +1 -0
  93. package/dist/mcp-server/src/provisioning/index.js +24 -0
  94. package/dist/mcp-server/src/provisioning/index.js.map +1 -0
  95. package/dist/mcp-server/src/provisioning/linux-user.d.ts +15 -0
  96. package/dist/mcp-server/src/provisioning/linux-user.d.ts.map +1 -0
  97. package/dist/mcp-server/src/provisioning/linux-user.js +62 -0
  98. package/dist/mcp-server/src/provisioning/linux-user.js.map +1 -0
  99. package/dist/mcp-server/src/provisioning/privileged.d.ts +40 -0
  100. package/dist/mcp-server/src/provisioning/privileged.d.ts.map +1 -0
  101. package/dist/mcp-server/src/provisioning/privileged.js +123 -0
  102. package/dist/mcp-server/src/provisioning/privileged.js.map +1 -0
  103. package/dist/mcp-server/src/provisioning/ssh-config.d.ts +21 -0
  104. package/dist/mcp-server/src/provisioning/ssh-config.d.ts.map +1 -0
  105. package/dist/mcp-server/src/provisioning/ssh-config.js +161 -0
  106. package/dist/mcp-server/src/provisioning/ssh-config.js.map +1 -0
  107. package/dist/mcp-server/src/provisioning/tmux-session.d.ts +37 -0
  108. package/dist/mcp-server/src/provisioning/tmux-session.d.ts.map +1 -0
  109. package/dist/mcp-server/src/provisioning/tmux-session.js +123 -0
  110. package/dist/mcp-server/src/provisioning/tmux-session.js.map +1 -0
  111. package/dist/mcp-server/src/server.d.ts +3 -0
  112. package/dist/mcp-server/src/server.d.ts.map +1 -0
  113. package/dist/mcp-server/src/server.js +67 -0
  114. package/dist/mcp-server/src/server.js.map +1 -0
  115. package/dist/mcp-server/src/state/gig-store.d.ts +19 -0
  116. package/dist/mcp-server/src/state/gig-store.d.ts.map +1 -0
  117. package/dist/mcp-server/src/state/gig-store.js +52 -0
  118. package/dist/mcp-server/src/state/gig-store.js.map +1 -0
  119. package/dist/mcp-server/src/state/index.d.ts +4 -0
  120. package/dist/mcp-server/src/state/index.d.ts.map +1 -0
  121. package/dist/mcp-server/src/state/index.js +8 -0
  122. package/dist/mcp-server/src/state/index.js.map +1 -0
  123. package/dist/mcp-server/src/state/persistence.d.ts +13 -0
  124. package/dist/mcp-server/src/state/persistence.d.ts.map +1 -0
  125. package/dist/mcp-server/src/state/persistence.js +48 -0
  126. package/dist/mcp-server/src/state/persistence.js.map +1 -0
  127. package/dist/mcp-server/src/state/types.d.ts +15 -0
  128. package/dist/mcp-server/src/state/types.d.ts.map +1 -0
  129. package/dist/mcp-server/src/state/types.js +3 -0
  130. package/dist/mcp-server/src/state/types.js.map +1 -0
  131. package/dist/mcp-server/src/tools/dismiss-human.d.ts +25 -0
  132. package/dist/mcp-server/src/tools/dismiss-human.d.ts.map +1 -0
  133. package/dist/mcp-server/src/tools/dismiss-human.js +78 -0
  134. package/dist/mcp-server/src/tools/dismiss-human.js.map +1 -0
  135. package/dist/mcp-server/src/tools/index.d.ts +9 -0
  136. package/dist/mcp-server/src/tools/index.d.ts.map +1 -0
  137. package/dist/mcp-server/src/tools/index.js +20 -0
  138. package/dist/mcp-server/src/tools/index.js.map +1 -0
  139. package/dist/mcp-server/src/tools/list-humans.d.ts +18 -0
  140. package/dist/mcp-server/src/tools/list-humans.d.ts.map +1 -0
  141. package/dist/mcp-server/src/tools/list-humans.js +35 -0
  142. package/dist/mcp-server/src/tools/list-humans.js.map +1 -0
  143. package/dist/mcp-server/src/tools/message-human.d.ts +10 -0
  144. package/dist/mcp-server/src/tools/message-human.d.ts.map +1 -0
  145. package/dist/mcp-server/src/tools/message-human.js +19 -0
  146. package/dist/mcp-server/src/tools/message-human.js.map +1 -0
  147. package/dist/mcp-server/src/tools/schemas/dismiss-human.schema.d.ts +19 -0
  148. package/dist/mcp-server/src/tools/schemas/dismiss-human.schema.d.ts.map +1 -0
  149. package/dist/mcp-server/src/tools/schemas/dismiss-human.schema.js +22 -0
  150. package/dist/mcp-server/src/tools/schemas/dismiss-human.schema.js.map +1 -0
  151. package/dist/mcp-server/src/tools/schemas/list-humans.schema.d.ts +4 -0
  152. package/dist/mcp-server/src/tools/schemas/list-humans.schema.d.ts.map +1 -0
  153. package/dist/mcp-server/src/tools/schemas/list-humans.schema.js +7 -0
  154. package/dist/mcp-server/src/tools/schemas/list-humans.schema.js.map +1 -0
  155. package/dist/mcp-server/src/tools/schemas/message-human.schema.d.ts +13 -0
  156. package/dist/mcp-server/src/tools/schemas/message-human.schema.d.ts.map +1 -0
  157. package/dist/mcp-server/src/tools/schemas/message-human.schema.js +9 -0
  158. package/dist/mcp-server/src/tools/schemas/message-human.schema.js.map +1 -0
  159. package/dist/mcp-server/src/tools/schemas/summon-human.schema.d.ts +22 -0
  160. package/dist/mcp-server/src/tools/schemas/summon-human.schema.d.ts.map +1 -0
  161. package/dist/mcp-server/src/tools/schemas/summon-human.schema.js +18 -0
  162. package/dist/mcp-server/src/tools/schemas/summon-human.schema.js.map +1 -0
  163. package/dist/mcp-server/src/tools/summon-human.d.ts +31 -0
  164. package/dist/mcp-server/src/tools/summon-human.d.ts.map +1 -0
  165. package/dist/mcp-server/src/tools/summon-human.js +137 -0
  166. package/dist/mcp-server/src/tools/summon-human.js.map +1 -0
  167. package/dist/mcp-server/src/tunnel/client.d.ts +16 -0
  168. package/dist/mcp-server/src/tunnel/client.d.ts.map +1 -0
  169. package/dist/mcp-server/src/tunnel/client.js +100 -0
  170. package/dist/mcp-server/src/tunnel/client.js.map +1 -0
  171. package/dist/mcp-server/src/tunnel/index.d.ts +6 -0
  172. package/dist/mcp-server/src/tunnel/index.d.ts.map +1 -0
  173. package/dist/mcp-server/src/tunnel/index.js +28 -0
  174. package/dist/mcp-server/src/tunnel/index.js.map +1 -0
  175. package/dist/mcp-server/src/utils/errors.d.ts +28 -0
  176. package/dist/mcp-server/src/utils/errors.d.ts.map +1 -0
  177. package/dist/mcp-server/src/utils/errors.js +66 -0
  178. package/dist/mcp-server/src/utils/errors.js.map +1 -0
  179. package/dist/mcp-server/src/utils/exec.d.ts +7 -0
  180. package/dist/mcp-server/src/utils/exec.d.ts.map +1 -0
  181. package/dist/mcp-server/src/utils/exec.js +22 -0
  182. package/dist/mcp-server/src/utils/exec.js.map +1 -0
  183. package/dist/mcp-server/src/utils/ip.d.ts +6 -0
  184. package/dist/mcp-server/src/utils/ip.d.ts.map +1 -0
  185. package/dist/mcp-server/src/utils/ip.js +33 -0
  186. package/dist/mcp-server/src/utils/ip.js.map +1 -0
  187. package/dist/mcp-server/src/utils/logger.d.ts +20 -0
  188. package/dist/mcp-server/src/utils/logger.d.ts.map +1 -0
  189. package/dist/mcp-server/src/utils/logger.js +41 -0
  190. package/dist/mcp-server/src/utils/logger.js.map +1 -0
  191. package/dist/shared/src/contractor.types.d.ts +20 -0
  192. package/dist/shared/src/contractor.types.d.ts.map +1 -0
  193. package/dist/shared/src/contractor.types.js +3 -0
  194. package/dist/shared/src/contractor.types.js.map +1 -0
  195. package/dist/shared/src/gig.types.d.ts +32 -0
  196. package/dist/shared/src/gig.types.d.ts.map +1 -0
  197. package/dist/shared/src/gig.types.js +21 -0
  198. package/dist/shared/src/gig.types.js.map +1 -0
  199. package/dist/shared/src/index.d.ts +5 -0
  200. package/dist/shared/src/index.d.ts.map +1 -0
  201. package/dist/shared/src/index.js +21 -0
  202. package/dist/shared/src/index.js.map +1 -0
  203. package/dist/shared/src/mcp-tool.types.d.ts +45 -0
  204. package/dist/shared/src/mcp-tool.types.d.ts.map +1 -0
  205. package/dist/shared/src/mcp-tool.types.js +3 -0
  206. package/dist/shared/src/mcp-tool.types.js.map +1 -0
  207. package/dist/shared/src/platform-api.types.d.ts +73 -0
  208. package/dist/shared/src/platform-api.types.d.ts.map +1 -0
  209. package/dist/shared/src/platform-api.types.js +3 -0
  210. package/dist/shared/src/platform-api.types.js.map +1 -0
  211. package/package.json +41 -0
  212. package/src/bin.ts +7 -0
  213. package/src/cli/setup.ts +317 -0
  214. package/src/config/defaults.ts +21 -0
  215. package/src/config/env.ts +74 -0
  216. package/src/config/index.ts +2 -0
  217. package/src/context/generator.ts +71 -0
  218. package/src/context/index.ts +6 -0
  219. package/src/context/templates.ts +41 -0
  220. package/src/git/branch.ts +46 -0
  221. package/src/git/diff.ts +34 -0
  222. package/src/git/index.ts +4 -0
  223. package/src/git/merge.ts +36 -0
  224. package/src/git/worktree.ts +42 -0
  225. package/src/http-wrapper.ts +94 -0
  226. package/src/index.ts +32 -0
  227. package/src/platform-client/client.ts +93 -0
  228. package/src/platform-client/index.ts +4 -0
  229. package/src/platform-client/mock-client.ts +92 -0
  230. package/src/platform-client/polling.ts +53 -0
  231. package/src/platform-client/types.ts +9 -0
  232. package/src/provisioning/authorized-keys.ts +52 -0
  233. package/src/provisioning/cleanup.ts +106 -0
  234. package/src/provisioning/index.ts +13 -0
  235. package/src/provisioning/linux-user.ts +66 -0
  236. package/src/provisioning/privileged.ts +128 -0
  237. package/src/provisioning/ssh-config.ts +197 -0
  238. package/src/provisioning/tmux-session.ts +136 -0
  239. package/src/server.ts +111 -0
  240. package/src/state/gig-store.ts +56 -0
  241. package/src/state/index.ts +3 -0
  242. package/src/state/persistence.ts +42 -0
  243. package/src/state/types.ts +14 -0
  244. package/src/tools/dismiss-human.ts +103 -0
  245. package/src/tools/index.ts +9 -0
  246. package/src/tools/list-humans.ts +54 -0
  247. package/src/tools/message-human.ts +28 -0
  248. package/src/tools/schemas/dismiss-human.schema.ts +21 -0
  249. package/src/tools/schemas/list-humans.schema.ts +6 -0
  250. package/src/tools/schemas/message-human.schema.ts +8 -0
  251. package/src/tools/schemas/summon-human.schema.ts +19 -0
  252. package/src/tools/summon-human.ts +180 -0
  253. package/src/tunnel/client.ts +116 -0
  254. package/src/tunnel/index.ts +26 -0
  255. package/src/utils/errors.ts +64 -0
  256. package/src/utils/exec.ts +29 -0
  257. package/src/utils/ip.ts +31 -0
  258. package/src/utils/logger.ts +55 -0
  259. package/tsconfig.json +20 -0
@@ -0,0 +1,103 @@
1
+ import type { PlatformClient } from "../platform-client/client.js";
2
+ import { cleanupContractor } from "../provisioning/cleanup.js";
3
+ import { mergeContractorChanges } from "../git/merge.js";
4
+ import { getFilesChanged } from "../git/diff.js";
5
+ import { removeWorktree } from "../git/worktree.js";
6
+ import { closeTunnel } from "../tunnel/index.js";
7
+ import { GigStore } from "../state/index.js";
8
+ import { logger } from "../utils/logger.js";
9
+ import type { DismissHumanInput } from "./schemas/dismiss-human.schema.js";
10
+
11
+ export interface DismissHumanDeps {
12
+ platformClient: PlatformClient;
13
+ gigStore: GigStore;
14
+ }
15
+
16
+ export interface DismissHumanResult {
17
+ filesChanged: string[];
18
+ merged: boolean;
19
+ durationMinutes: number;
20
+ }
21
+
22
+ /**
23
+ * dismiss_human — clean up a contractor's access and report completion.
24
+ *
25
+ * 1. Update gig status to "dismissing"
26
+ * 2. Optionally merge contractor's changes
27
+ * 3. Get list of files changed
28
+ * 4. Run aggressive cleanup (tmux, processes, SSH, lock account)
29
+ * 5. Remove worktree if "clean" mode was used
30
+ * 6. Report completion to platform
31
+ * 7. Remove gig from store
32
+ */
33
+ export async function dismissHuman(
34
+ input: DismissHumanInput,
35
+ deps: DismissHumanDeps,
36
+ ): Promise<DismissHumanResult> {
37
+ const gig = deps.gigStore.get(input.gigId);
38
+ gig.status = "dismissing";
39
+
40
+ logger.info("Dismissing human contractor", {
41
+ gigId: input.gigId,
42
+ contractorName: gig.contractorName,
43
+ merge: input.merge,
44
+ });
45
+
46
+ // Calculate duration
47
+ const startedAt = new Date(gig.startedAt);
48
+ const durationMinutes = Math.round(
49
+ (Date.now() - startedAt.getTime()) / 60_000,
50
+ );
51
+
52
+ // Merge if requested
53
+ let filesChanged: string[] = [];
54
+ if (input.merge && gig.worktreePath) {
55
+ await mergeContractorChanges(gig.worktreePath);
56
+ filesChanged = await getFilesChanged();
57
+ } else if (input.merge) {
58
+ // Shared worktree — changes are already in place
59
+ filesChanged = await getFilesChanged();
60
+ }
61
+
62
+ // Close reverse tunnel if active
63
+ if (gig.tunnelActive) {
64
+ closeTunnel(input.gigId);
65
+ }
66
+
67
+ // Aggressive cleanup
68
+ await cleanupContractor({
69
+ username: gig.linuxUser,
70
+ tmuxSession: gig.tmuxSession,
71
+ });
72
+
73
+ // Remove worktree if "clean" mode
74
+ if (gig.worktreePath) {
75
+ await removeWorktree(gig.worktreePath);
76
+ }
77
+
78
+ // Report to platform
79
+ await deps.platformClient.completeGig({
80
+ gigId: input.gigId,
81
+ durationMinutes,
82
+ filesChanged,
83
+ merged: input.merge,
84
+ rating: input.rating,
85
+ resolutionNotes: input.resolutionNotes,
86
+ });
87
+
88
+ // Remove from store
89
+ deps.gigStore.remove(input.gigId);
90
+
91
+ logger.info("Human contractor dismissed", {
92
+ gigId: input.gigId,
93
+ durationMinutes,
94
+ filesChanged: filesChanged.length,
95
+ merged: input.merge,
96
+ });
97
+
98
+ return {
99
+ filesChanged,
100
+ merged: input.merge,
101
+ durationMinutes,
102
+ };
103
+ }
@@ -0,0 +1,9 @@
1
+ export { summonHuman, type SummonHumanDeps, type SummonHumanResult } from "./summon-human.js";
2
+ export { dismissHuman, type DismissHumanDeps, type DismissHumanResult } from "./dismiss-human.js";
3
+ export { listHumans, type ListHumansResult } from "./list-humans.js";
4
+ export { messageHuman, type MessageHumanResult } from "./message-human.js";
5
+
6
+ export { SummonHumanInputSchema } from "./schemas/summon-human.schema.js";
7
+ export { DismissHumanInputSchema } from "./schemas/dismiss-human.schema.js";
8
+ export { ListHumansInputSchema } from "./schemas/list-humans.schema.js";
9
+ export { MessageHumanInputSchema } from "./schemas/message-human.schema.js";
@@ -0,0 +1,54 @@
1
+ import { GigStore } from "../state/index.js";
2
+ import {
3
+ hasClientsAttached,
4
+ getLastActivity,
5
+ } from "../provisioning/tmux-session.js";
6
+
7
+ export interface ActiveContractorInfo {
8
+ gigId: string;
9
+ name: string;
10
+ status: "waiting" | "connected" | "idle";
11
+ skills: string[];
12
+ connectedSince?: string;
13
+ lastActivity?: string;
14
+ rate: number;
15
+ }
16
+
17
+ export interface ListHumansResult {
18
+ contractors: ActiveContractorInfo[];
19
+ }
20
+
21
+ /**
22
+ * list_humans — show currently active contractors on this VM.
23
+ */
24
+ export async function listHumans(gigStore: GigStore): Promise<ListHumansResult> {
25
+ const gigs = gigStore.list();
26
+
27
+ const contractors: ActiveContractorInfo[] = await Promise.all(
28
+ gigs.map(async (gig) => {
29
+ const connected = await hasClientsAttached(gig.tmuxSession);
30
+ const lastActivity = await getLastActivity(gig.tmuxSession);
31
+
32
+ let status: "waiting" | "connected" | "idle";
33
+ if (!connected && gig.status === "active") {
34
+ status = "waiting";
35
+ } else if (connected) {
36
+ status = "connected";
37
+ } else {
38
+ status = "idle";
39
+ }
40
+
41
+ return {
42
+ gigId: gig.gigId,
43
+ name: gig.contractorName,
44
+ status,
45
+ skills: gig.skills,
46
+ connectedSince: gig.startedAt,
47
+ lastActivity: lastActivity?.toISOString(),
48
+ rate: gig.rate,
49
+ };
50
+ }),
51
+ );
52
+
53
+ return { contractors };
54
+ }
@@ -0,0 +1,28 @@
1
+ import { GigStore } from "../state/index.js";
2
+ import { displayMessage } from "../provisioning/tmux-session.js";
3
+ import { logger } from "../utils/logger.js";
4
+ import type { MessageHumanInput } from "./schemas/message-human.schema.js";
5
+
6
+ export interface MessageHumanResult {
7
+ delivered: boolean;
8
+ }
9
+
10
+ /**
11
+ * message_human — send a message into the contractor's tmux session.
12
+ */
13
+ export async function messageHuman(
14
+ input: MessageHumanInput,
15
+ gigStore: GigStore,
16
+ ): Promise<MessageHumanResult> {
17
+ const gig = gigStore.get(input.gigId);
18
+
19
+ const delivered = await displayMessage(gig.tmuxSession, input.message);
20
+
21
+ logger.info("Message sent to contractor", {
22
+ gigId: input.gigId,
23
+ contractorName: gig.contractorName,
24
+ delivered,
25
+ });
26
+
27
+ return { delivered };
28
+ }
@@ -0,0 +1,21 @@
1
+ import { z } from "zod";
2
+
3
+ export const DismissHumanInputSchema = z.object({
4
+ gigId: z.string().describe("The gig ID to dismiss"),
5
+ merge: z
6
+ .boolean()
7
+ .describe("Whether to merge contractor's changes into working branch"),
8
+ rating: z
9
+ .number()
10
+ .int()
11
+ .min(1)
12
+ .max(5)
13
+ .optional()
14
+ .describe("Rating 1-5 (optional, user can rate later via platform)"),
15
+ resolutionNotes: z
16
+ .string()
17
+ .optional()
18
+ .describe("What was fixed (optional)"),
19
+ });
20
+
21
+ export type DismissHumanInput = z.infer<typeof DismissHumanInputSchema>;
@@ -0,0 +1,6 @@
1
+ import { z } from "zod";
2
+
3
+ // list_humans takes no input
4
+ export const ListHumansInputSchema = z.object({});
5
+
6
+ export type ListHumansInput = z.infer<typeof ListHumansInputSchema>;
@@ -0,0 +1,8 @@
1
+ import { z } from "zod";
2
+
3
+ export const MessageHumanInputSchema = z.object({
4
+ gigId: z.string().describe("The gig ID of the contractor to message"),
5
+ message: z.string().describe("The message to send"),
6
+ });
7
+
8
+ export type MessageHumanInput = z.infer<typeof MessageHumanInputSchema>;
@@ -0,0 +1,19 @@
1
+ import { z } from "zod";
2
+
3
+ export const SummonHumanInputSchema = z.object({
4
+ reason: z.string().describe("Why the AI needs help"),
5
+ skills: z.array(z.string()).describe("What expertise is needed"),
6
+ context: z
7
+ .string()
8
+ .describe("What was tried, what failed, relevant files"),
9
+ urgency: z
10
+ .enum(["low", "medium", "high"])
11
+ .describe("How urgent is this request"),
12
+ worktree: z
13
+ .enum(["shared", "clean"])
14
+ .describe(
15
+ "shared = human sees AI's dirty working state, clean = fresh checkout",
16
+ ),
17
+ });
18
+
19
+ export type SummonHumanInput = z.infer<typeof SummonHumanInputSchema>;
@@ -0,0 +1,180 @@
1
+ import type { PlatformClient } from "../platform-client/client.js";
2
+ import { pollForAssignment } from "../platform-client/polling.js";
3
+ import {
4
+ createUser,
5
+ writeAuthorizedKey,
6
+ addForceCommand,
7
+ sessionName,
8
+ createSession,
9
+ } from "../provisioning/index.js";
10
+ import { getCurrentBranch, getRepoName, getRepoRoot } from "../git/index.js";
11
+ import { createWorktree } from "../git/worktree.js";
12
+ import { generateContext } from "../context/index.js";
13
+ import { GigStore } from "../state/index.js";
14
+ import { getEnv } from "../config/env.js";
15
+ import { getExternalIp } from "../utils/ip.js";
16
+ import { openTunnel, storeTunnel } from "../tunnel/index.js";
17
+ import { logger } from "../utils/logger.js";
18
+ import type { SummonHumanInput } from "./schemas/summon-human.schema.js";
19
+ import type { Urgency } from "../../../shared/src/gig.types.js";
20
+
21
+ export interface SummonHumanDeps {
22
+ platformClient: PlatformClient;
23
+ gigStore: GigStore;
24
+ }
25
+
26
+ export interface SummonHumanResult {
27
+ gigId: string;
28
+ contractorName: string;
29
+ sshCommand: string;
30
+ connectionMode: "tunnel" | "direct";
31
+ status: "waiting_for_connection";
32
+ }
33
+
34
+ /**
35
+ * summon_human — the most complex MCP tool.
36
+ *
37
+ * Orchestrates the full flow:
38
+ * 1. Detect repo info
39
+ * 2. POST gig to platform
40
+ * 3. Poll for contractor assignment
41
+ * 4. Create Linux user
42
+ * 5. Write SSH pubkey
43
+ * 6. Configure ForceCommand in sshd_config
44
+ * 7. Create tmux session
45
+ * 8. Write CONTEXT.md
46
+ * 9. Notify platform that VM is ready
47
+ * 10. Store active gig
48
+ */
49
+ export async function summonHuman(
50
+ input: SummonHumanInput,
51
+ deps: SummonHumanDeps,
52
+ ): Promise<SummonHumanResult> {
53
+ const env = getEnv();
54
+
55
+ // 1. Detect repo info
56
+ const repoRoot = await getRepoRoot();
57
+ const branch = await getCurrentBranch();
58
+ const repo = await getRepoName();
59
+ const vmIp =
60
+ env.VM_EXTERNAL_IP === "auto"
61
+ ? await getExternalIp()
62
+ : env.VM_EXTERNAL_IP;
63
+ const vmPort = env.VM_EXTERNAL_SSH_PORT;
64
+
65
+ logger.info("Summoning human contractor", {
66
+ reason: input.reason,
67
+ skills: input.skills.join(", "),
68
+ urgency: input.urgency,
69
+ });
70
+
71
+ // 2. Create gig on platform.
72
+ // The MCP tool schema accepts lowercase urgency for readability
73
+ // ("low" / "medium" / "high" reads better in LLM tool descriptions),
74
+ // but the human-layer API Urgency type is uppercase. Translate here.
75
+ const { gigId } = await deps.platformClient.createGig({
76
+ reason: input.reason,
77
+ skills: input.skills,
78
+ context: input.context,
79
+ urgency: input.urgency.toUpperCase() as Urgency,
80
+ worktreeMode: input.worktree,
81
+ vmIp,
82
+ repo,
83
+ branch,
84
+ projectId: env.PLATFORM_PROJECT_ID,
85
+ });
86
+
87
+ // 3. Poll for contractor assignment
88
+ const status = await pollForAssignment(deps.platformClient, gigId);
89
+ const contractor = status.contractor!;
90
+
91
+ logger.info("Contractor assigned, provisioning access", {
92
+ gigId,
93
+ contractorName: contractor.name,
94
+ });
95
+
96
+ // 4. Create Linux user
97
+ await createUser(contractor.name);
98
+
99
+ // 5. Write SSH pubkey
100
+ await writeAuthorizedKey(contractor.name, contractor.pubkey);
101
+
102
+ // 6. Determine working directory
103
+ const tmuxSessionId = sessionName(gigId);
104
+ let worktreePath = repoRoot;
105
+
106
+ if (input.worktree === "clean") {
107
+ worktreePath = `/tmp/hl-worktree-${gigId}`;
108
+ await createWorktree(branch, worktreePath);
109
+ }
110
+
111
+ // 7. Create tmux session — owned by the contractor user so their
112
+ // SSH ForceCommand can attach to it. tmux scopes sessions per uid.
113
+ await createSession(tmuxSessionId, worktreePath, contractor.name);
114
+
115
+ // 8. Configure ForceCommand
116
+ await addForceCommand(contractor.name, tmuxSessionId);
117
+
118
+ // 9. Write CONTEXT.md
119
+ await generateContext({
120
+ reason: input.reason,
121
+ context: input.context,
122
+ skills: input.skills,
123
+ urgency: input.urgency,
124
+ repo,
125
+ branch,
126
+ worktreePath,
127
+ });
128
+
129
+ // 10. Set up connectivity: reverse tunnel or direct SSH
130
+ const useTunnel = env.TUNNEL_ENABLED;
131
+ let sshCommand: string;
132
+
133
+ if (useTunnel) {
134
+ // Open reverse tunnel — platform routes contractor traffic through it
135
+ const platformWsUrl = env.PLATFORM_API_URL.replace(/^http/, "ws");
136
+ const handle = openTunnel(platformWsUrl, env.PLATFORM_API_KEY, gigId);
137
+ storeTunnel(gigId, handle);
138
+
139
+ sshCommand = `(via tunnel to platform)`;
140
+
141
+ logger.info("Reverse tunnel opened", { gigId });
142
+ } else {
143
+ // Legacy direct-SSH mode: build SSH command with public IP/port
144
+ sshCommand =
145
+ vmPort === 22
146
+ ? `ssh ${contractor.name}@${vmIp}`
147
+ : `ssh ${contractor.name}@${vmIp} -p ${vmPort}`;
148
+ }
149
+
150
+ await deps.platformClient.markReady(gigId, sshCommand);
151
+
152
+ // 11. Store active gig
153
+ deps.gigStore.add({
154
+ gigId,
155
+ contractorName: contractor.name,
156
+ linuxUser: contractor.name,
157
+ tmuxSession: tmuxSessionId,
158
+ worktreePath: input.worktree === "clean" ? worktreePath : undefined,
159
+ sshCommand,
160
+ skills: input.skills,
161
+ rate: contractor.rate,
162
+ startedAt: new Date().toISOString(),
163
+ status: "active",
164
+ tunnelActive: useTunnel,
165
+ });
166
+
167
+ logger.info("Human contractor summoned successfully", {
168
+ gigId,
169
+ contractorName: contractor.name,
170
+ connectionMode: useTunnel ? "tunnel" : "direct",
171
+ });
172
+
173
+ return {
174
+ gigId,
175
+ contractorName: contractor.name,
176
+ sshCommand,
177
+ connectionMode: useTunnel ? "tunnel" : "direct",
178
+ status: "waiting_for_connection",
179
+ };
180
+ }
@@ -0,0 +1,116 @@
1
+ import { WebSocket } from "ws";
2
+ import { createConnection, type Socket } from "node:net";
3
+ import { logger } from "../utils/logger.js";
4
+
5
+ export interface TunnelHandle {
6
+ close(): void;
7
+ }
8
+
9
+ /**
10
+ * Open a reverse tunnel WebSocket to the platform.
11
+ *
12
+ * The platform will route contractor web-terminal traffic through this
13
+ * tunnel. When data arrives, we relay it to localhost:22 (sshd) and
14
+ * send responses back through the WebSocket.
15
+ *
16
+ * The connection is lazy: the TCP socket to localhost:22 is only
17
+ * created when the first data frame arrives (triggered by the
18
+ * platform's ssh2 client initiating a handshake).
19
+ */
20
+ export function openTunnel(
21
+ platformWsUrl: string,
22
+ apiKey: string,
23
+ gigId: string,
24
+ ): TunnelHandle {
25
+ const wsUrl = `${platformWsUrl}/ws/tunnel?gigId=${encodeURIComponent(gigId)}&token=${encodeURIComponent(apiKey)}`;
26
+
27
+ let ws: WebSocket | null = null;
28
+ let tcp: Socket | null = null;
29
+ let closed = false;
30
+
31
+ function connectWs(): void {
32
+ if (closed) return;
33
+
34
+ ws = new WebSocket(wsUrl);
35
+ ws.binaryType = "nodebuffer";
36
+
37
+ ws.on("open", () => {
38
+ logger.info("Tunnel WebSocket connected", { gigId });
39
+ });
40
+
41
+ ws.on("message", (data: Buffer) => {
42
+ if (!tcp) {
43
+ // Lazy-open TCP to local sshd on first data
44
+ tcp = createConnection({ host: "127.0.0.1", port: 22 }, () => {
45
+ logger.info("Tunnel TCP connected to local sshd", { gigId });
46
+ // Send the buffered first frame
47
+ tcp!.write(data);
48
+ });
49
+
50
+ tcp.on("data", (chunk: Buffer) => {
51
+ if (ws && ws.readyState === WebSocket.OPEN) {
52
+ ws.send(chunk);
53
+ }
54
+ });
55
+
56
+ tcp.on("close", () => {
57
+ logger.info("Tunnel TCP closed", { gigId });
58
+ tcp = null;
59
+ // Don't close the WebSocket — a new terminal session may
60
+ // open another TCP connection via this same tunnel.
61
+ });
62
+
63
+ tcp.on("error", (err) => {
64
+ logger.warn("Tunnel TCP error", {
65
+ gigId,
66
+ error: err.message,
67
+ });
68
+ tcp?.destroy();
69
+ tcp = null;
70
+ });
71
+ } else {
72
+ tcp.write(data);
73
+ }
74
+ });
75
+
76
+ ws.on("close", (code, reason) => {
77
+ logger.info("Tunnel WebSocket closed", {
78
+ gigId,
79
+ code,
80
+ reason: reason.toString(),
81
+ });
82
+ tcp?.destroy();
83
+ tcp = null;
84
+
85
+ // Reconnect with backoff unless explicitly closed
86
+ if (!closed) {
87
+ const delay = 3_000 + Math.random() * 2_000;
88
+ logger.info("Tunnel reconnecting", { gigId, delayMs: Math.round(delay) });
89
+ setTimeout(connectWs, delay);
90
+ }
91
+ });
92
+
93
+ ws.on("error", (err) => {
94
+ logger.warn("Tunnel WebSocket error", {
95
+ gigId,
96
+ error: err.message,
97
+ });
98
+ // on("close") will fire after this and handle reconnection
99
+ });
100
+ }
101
+
102
+ connectWs();
103
+
104
+ return {
105
+ close() {
106
+ closed = true;
107
+ tcp?.destroy();
108
+ tcp = null;
109
+ if (ws && ws.readyState === WebSocket.OPEN) {
110
+ ws.close(1000, "Tunnel closed by MCP server");
111
+ }
112
+ ws = null;
113
+ logger.info("Tunnel closed", { gigId });
114
+ },
115
+ };
116
+ }
@@ -0,0 +1,26 @@
1
+ import type { TunnelHandle } from "./client.js";
2
+
3
+ export { openTunnel, type TunnelHandle } from "./client.js";
4
+
5
+ /**
6
+ * In-memory map of active tunnel handles keyed by gigId.
7
+ * Not serialized — tunnels are re-established on restart via
8
+ * the MCP server's crash-recovery flow if needed.
9
+ */
10
+ const activeTunnels = new Map<string, TunnelHandle>();
11
+
12
+ export function storeTunnel(gigId: string, handle: TunnelHandle): void {
13
+ activeTunnels.set(gigId, handle);
14
+ }
15
+
16
+ export function closeTunnel(gigId: string): void {
17
+ const handle = activeTunnels.get(gigId);
18
+ if (handle) {
19
+ handle.close();
20
+ activeTunnels.delete(gigId);
21
+ }
22
+ }
23
+
24
+ export function hasTunnel(gigId: string): boolean {
25
+ return activeTunnels.has(gigId);
26
+ }
@@ -0,0 +1,64 @@
1
+ export class HumanLayerError extends Error {
2
+ constructor(
3
+ message: string,
4
+ public readonly code: string,
5
+ ) {
6
+ super(message);
7
+ this.name = "HumanLayerError";
8
+ }
9
+ }
10
+
11
+ export class ProvisioningError extends HumanLayerError {
12
+ constructor(message: string) {
13
+ super(message, "PROVISIONING_ERROR");
14
+ this.name = "ProvisioningError";
15
+ }
16
+ }
17
+
18
+ export class SshConfigError extends HumanLayerError {
19
+ constructor(message: string) {
20
+ super(message, "SSH_CONFIG_ERROR");
21
+ this.name = "SshConfigError";
22
+ }
23
+ }
24
+
25
+ export class PlatformApiError extends HumanLayerError {
26
+ constructor(
27
+ message: string,
28
+ public readonly statusCode?: number,
29
+ ) {
30
+ super(message, "PLATFORM_API_ERROR");
31
+ this.name = "PlatformApiError";
32
+ }
33
+ }
34
+
35
+ export class GigNotFoundError extends HumanLayerError {
36
+ constructor(gigId: string) {
37
+ super(`Gig not found: ${gigId}`, "GIG_NOT_FOUND");
38
+ this.name = "GigNotFoundError";
39
+ }
40
+ }
41
+
42
+ export class CleanupError extends HumanLayerError {
43
+ constructor(
44
+ message: string,
45
+ public readonly partialFailures: string[],
46
+ ) {
47
+ super(message, "CLEANUP_ERROR");
48
+ this.name = "CleanupError";
49
+ }
50
+ }
51
+
52
+ export class TmuxError extends HumanLayerError {
53
+ constructor(message: string) {
54
+ super(message, "TMUX_ERROR");
55
+ this.name = "TmuxError";
56
+ }
57
+ }
58
+
59
+ export class GitError extends HumanLayerError {
60
+ constructor(message: string) {
61
+ super(message, "GIT_ERROR");
62
+ this.name = "GitError";
63
+ }
64
+ }
@@ -0,0 +1,29 @@
1
+ import { exec as cpExec, type ExecOptions } from "node:child_process";
2
+
3
+ export interface ExecResult {
4
+ stdout: string;
5
+ stderr: string;
6
+ }
7
+
8
+ export function exec(
9
+ command: string,
10
+ options?: ExecOptions,
11
+ ): Promise<ExecResult> {
12
+ return new Promise((resolve, reject) => {
13
+ cpExec(command, { timeout: 30_000, ...options }, (error, stdout, stderr) => {
14
+ if (error) {
15
+ reject(
16
+ Object.assign(error, {
17
+ stdout: stdout?.toString() ?? "",
18
+ stderr: stderr?.toString() ?? "",
19
+ }),
20
+ );
21
+ return;
22
+ }
23
+ resolve({
24
+ stdout: stdout?.toString() ?? "",
25
+ stderr: stderr?.toString() ?? "",
26
+ });
27
+ });
28
+ });
29
+ }