@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.
- package/.env.example +15 -0
- package/README.md +178 -0
- package/dist/mcp-server/src/bin.d.ts +3 -0
- package/dist/mcp-server/src/bin.d.ts.map +1 -0
- package/dist/mcp-server/src/bin.js +10 -0
- package/dist/mcp-server/src/bin.js.map +1 -0
- package/dist/mcp-server/src/cli/setup.d.ts +2 -0
- package/dist/mcp-server/src/cli/setup.d.ts.map +1 -0
- package/dist/mcp-server/src/cli/setup.js +274 -0
- package/dist/mcp-server/src/cli/setup.js.map +1 -0
- package/dist/mcp-server/src/config/defaults.d.ts +16 -0
- package/dist/mcp-server/src/config/defaults.d.ts.map +1 -0
- package/dist/mcp-server/src/config/defaults.js +19 -0
- package/dist/mcp-server/src/config/defaults.js.map +1 -0
- package/dist/mcp-server/src/config/env.d.ts +73 -0
- package/dist/mcp-server/src/config/env.d.ts.map +1 -0
- package/dist/mcp-server/src/config/env.js +72 -0
- package/dist/mcp-server/src/config/env.js.map +1 -0
- package/dist/mcp-server/src/config/index.d.ts +3 -0
- package/dist/mcp-server/src/config/index.d.ts.map +1 -0
- package/dist/mcp-server/src/config/index.js +22 -0
- package/dist/mcp-server/src/config/index.js.map +1 -0
- package/dist/mcp-server/src/context/generator.d.ts +16 -0
- package/dist/mcp-server/src/context/generator.d.ts.map +1 -0
- package/dist/mcp-server/src/context/generator.js +61 -0
- package/dist/mcp-server/src/context/generator.js.map +1 -0
- package/dist/mcp-server/src/context/index.d.ts +3 -0
- package/dist/mcp-server/src/context/index.d.ts.map +1 -0
- package/dist/mcp-server/src/context/index.js +10 -0
- package/dist/mcp-server/src/context/index.js.map +1 -0
- package/dist/mcp-server/src/context/templates.d.ts +8 -0
- package/dist/mcp-server/src/context/templates.d.ts.map +1 -0
- package/dist/mcp-server/src/context/templates.js +41 -0
- package/dist/mcp-server/src/context/templates.js.map +1 -0
- package/dist/mcp-server/src/git/branch.d.ts +13 -0
- package/dist/mcp-server/src/git/branch.d.ts.map +1 -0
- package/dist/mcp-server/src/git/branch.js +49 -0
- package/dist/mcp-server/src/git/branch.js.map +1 -0
- package/dist/mcp-server/src/git/diff.d.ts +10 -0
- package/dist/mcp-server/src/git/diff.d.ts.map +1 -0
- package/dist/mcp-server/src/git/diff.js +39 -0
- package/dist/mcp-server/src/git/diff.js.map +1 -0
- package/dist/mcp-server/src/git/index.d.ts +5 -0
- package/dist/mcp-server/src/git/index.d.ts.map +1 -0
- package/dist/mcp-server/src/git/index.js +16 -0
- package/dist/mcp-server/src/git/index.js.map +1 -0
- package/dist/mcp-server/src/git/merge.d.ts +6 -0
- package/dist/mcp-server/src/git/merge.d.ts.map +1 -0
- package/dist/mcp-server/src/git/merge.js +30 -0
- package/dist/mcp-server/src/git/merge.js.map +1 -0
- package/dist/mcp-server/src/git/worktree.d.ts +11 -0
- package/dist/mcp-server/src/git/worktree.d.ts.map +1 -0
- package/dist/mcp-server/src/git/worktree.js +38 -0
- package/dist/mcp-server/src/git/worktree.js.map +1 -0
- package/dist/mcp-server/src/http-wrapper.d.ts +6 -0
- package/dist/mcp-server/src/http-wrapper.d.ts.map +1 -0
- package/dist/mcp-server/src/http-wrapper.js +85 -0
- package/dist/mcp-server/src/http-wrapper.js.map +1 -0
- package/dist/mcp-server/src/index.d.ts +2 -0
- package/dist/mcp-server/src/index.d.ts.map +1 -0
- package/dist/mcp-server/src/index.js +28 -0
- package/dist/mcp-server/src/index.js.map +1 -0
- package/dist/mcp-server/src/platform-client/client.d.ts +17 -0
- package/dist/mcp-server/src/platform-client/client.d.ts.map +1 -0
- package/dist/mcp-server/src/platform-client/client.js +68 -0
- package/dist/mcp-server/src/platform-client/client.js.map +1 -0
- package/dist/mcp-server/src/platform-client/index.d.ts +5 -0
- package/dist/mcp-server/src/platform-client/index.d.ts.map +1 -0
- package/dist/mcp-server/src/platform-client/index.js +10 -0
- package/dist/mcp-server/src/platform-client/index.js.map +1 -0
- package/dist/mcp-server/src/platform-client/mock-client.d.ts +28 -0
- package/dist/mcp-server/src/platform-client/mock-client.d.ts.map +1 -0
- package/dist/mcp-server/src/platform-client/mock-client.js +75 -0
- package/dist/mcp-server/src/platform-client/mock-client.js.map +1 -0
- package/dist/mcp-server/src/platform-client/polling.d.ts +9 -0
- package/dist/mcp-server/src/platform-client/polling.d.ts.map +1 -0
- package/dist/mcp-server/src/platform-client/polling.js +40 -0
- package/dist/mcp-server/src/platform-client/polling.js.map +1 -0
- package/dist/mcp-server/src/platform-client/types.d.ts +2 -0
- package/dist/mcp-server/src/platform-client/types.d.ts.map +1 -0
- package/dist/mcp-server/src/platform-client/types.js +3 -0
- package/dist/mcp-server/src/platform-client/types.js.map +1 -0
- package/dist/mcp-server/src/provisioning/authorized-keys.d.ts +14 -0
- package/dist/mcp-server/src/provisioning/authorized-keys.d.ts.map +1 -0
- package/dist/mcp-server/src/provisioning/authorized-keys.js +48 -0
- package/dist/mcp-server/src/provisioning/authorized-keys.js.map +1 -0
- package/dist/mcp-server/src/provisioning/cleanup.d.ts +19 -0
- package/dist/mcp-server/src/provisioning/cleanup.d.ts.map +1 -0
- package/dist/mcp-server/src/provisioning/cleanup.js +96 -0
- package/dist/mcp-server/src/provisioning/cleanup.js.map +1 -0
- package/dist/mcp-server/src/provisioning/index.d.ts +6 -0
- package/dist/mcp-server/src/provisioning/index.d.ts.map +1 -0
- package/dist/mcp-server/src/provisioning/index.js +24 -0
- package/dist/mcp-server/src/provisioning/index.js.map +1 -0
- package/dist/mcp-server/src/provisioning/linux-user.d.ts +15 -0
- package/dist/mcp-server/src/provisioning/linux-user.d.ts.map +1 -0
- package/dist/mcp-server/src/provisioning/linux-user.js +62 -0
- package/dist/mcp-server/src/provisioning/linux-user.js.map +1 -0
- package/dist/mcp-server/src/provisioning/privileged.d.ts +40 -0
- package/dist/mcp-server/src/provisioning/privileged.d.ts.map +1 -0
- package/dist/mcp-server/src/provisioning/privileged.js +123 -0
- package/dist/mcp-server/src/provisioning/privileged.js.map +1 -0
- package/dist/mcp-server/src/provisioning/ssh-config.d.ts +21 -0
- package/dist/mcp-server/src/provisioning/ssh-config.d.ts.map +1 -0
- package/dist/mcp-server/src/provisioning/ssh-config.js +161 -0
- package/dist/mcp-server/src/provisioning/ssh-config.js.map +1 -0
- package/dist/mcp-server/src/provisioning/tmux-session.d.ts +37 -0
- package/dist/mcp-server/src/provisioning/tmux-session.d.ts.map +1 -0
- package/dist/mcp-server/src/provisioning/tmux-session.js +123 -0
- package/dist/mcp-server/src/provisioning/tmux-session.js.map +1 -0
- package/dist/mcp-server/src/server.d.ts +3 -0
- package/dist/mcp-server/src/server.d.ts.map +1 -0
- package/dist/mcp-server/src/server.js +67 -0
- package/dist/mcp-server/src/server.js.map +1 -0
- package/dist/mcp-server/src/state/gig-store.d.ts +19 -0
- package/dist/mcp-server/src/state/gig-store.d.ts.map +1 -0
- package/dist/mcp-server/src/state/gig-store.js +52 -0
- package/dist/mcp-server/src/state/gig-store.js.map +1 -0
- package/dist/mcp-server/src/state/index.d.ts +4 -0
- package/dist/mcp-server/src/state/index.d.ts.map +1 -0
- package/dist/mcp-server/src/state/index.js +8 -0
- package/dist/mcp-server/src/state/index.js.map +1 -0
- package/dist/mcp-server/src/state/persistence.d.ts +13 -0
- package/dist/mcp-server/src/state/persistence.d.ts.map +1 -0
- package/dist/mcp-server/src/state/persistence.js +48 -0
- package/dist/mcp-server/src/state/persistence.js.map +1 -0
- package/dist/mcp-server/src/state/types.d.ts +15 -0
- package/dist/mcp-server/src/state/types.d.ts.map +1 -0
- package/dist/mcp-server/src/state/types.js +3 -0
- package/dist/mcp-server/src/state/types.js.map +1 -0
- package/dist/mcp-server/src/tools/dismiss-human.d.ts +25 -0
- package/dist/mcp-server/src/tools/dismiss-human.d.ts.map +1 -0
- package/dist/mcp-server/src/tools/dismiss-human.js +78 -0
- package/dist/mcp-server/src/tools/dismiss-human.js.map +1 -0
- package/dist/mcp-server/src/tools/index.d.ts +9 -0
- package/dist/mcp-server/src/tools/index.d.ts.map +1 -0
- package/dist/mcp-server/src/tools/index.js +20 -0
- package/dist/mcp-server/src/tools/index.js.map +1 -0
- package/dist/mcp-server/src/tools/list-humans.d.ts +18 -0
- package/dist/mcp-server/src/tools/list-humans.d.ts.map +1 -0
- package/dist/mcp-server/src/tools/list-humans.js +35 -0
- package/dist/mcp-server/src/tools/list-humans.js.map +1 -0
- package/dist/mcp-server/src/tools/message-human.d.ts +10 -0
- package/dist/mcp-server/src/tools/message-human.d.ts.map +1 -0
- package/dist/mcp-server/src/tools/message-human.js +19 -0
- package/dist/mcp-server/src/tools/message-human.js.map +1 -0
- package/dist/mcp-server/src/tools/schemas/dismiss-human.schema.d.ts +19 -0
- package/dist/mcp-server/src/tools/schemas/dismiss-human.schema.d.ts.map +1 -0
- package/dist/mcp-server/src/tools/schemas/dismiss-human.schema.js +22 -0
- package/dist/mcp-server/src/tools/schemas/dismiss-human.schema.js.map +1 -0
- package/dist/mcp-server/src/tools/schemas/list-humans.schema.d.ts +4 -0
- package/dist/mcp-server/src/tools/schemas/list-humans.schema.d.ts.map +1 -0
- package/dist/mcp-server/src/tools/schemas/list-humans.schema.js +7 -0
- package/dist/mcp-server/src/tools/schemas/list-humans.schema.js.map +1 -0
- package/dist/mcp-server/src/tools/schemas/message-human.schema.d.ts +13 -0
- package/dist/mcp-server/src/tools/schemas/message-human.schema.d.ts.map +1 -0
- package/dist/mcp-server/src/tools/schemas/message-human.schema.js +9 -0
- package/dist/mcp-server/src/tools/schemas/message-human.schema.js.map +1 -0
- package/dist/mcp-server/src/tools/schemas/summon-human.schema.d.ts +22 -0
- package/dist/mcp-server/src/tools/schemas/summon-human.schema.d.ts.map +1 -0
- package/dist/mcp-server/src/tools/schemas/summon-human.schema.js +18 -0
- package/dist/mcp-server/src/tools/schemas/summon-human.schema.js.map +1 -0
- package/dist/mcp-server/src/tools/summon-human.d.ts +31 -0
- package/dist/mcp-server/src/tools/summon-human.d.ts.map +1 -0
- package/dist/mcp-server/src/tools/summon-human.js +137 -0
- package/dist/mcp-server/src/tools/summon-human.js.map +1 -0
- package/dist/mcp-server/src/tunnel/client.d.ts +16 -0
- package/dist/mcp-server/src/tunnel/client.d.ts.map +1 -0
- package/dist/mcp-server/src/tunnel/client.js +100 -0
- package/dist/mcp-server/src/tunnel/client.js.map +1 -0
- package/dist/mcp-server/src/tunnel/index.d.ts +6 -0
- package/dist/mcp-server/src/tunnel/index.d.ts.map +1 -0
- package/dist/mcp-server/src/tunnel/index.js +28 -0
- package/dist/mcp-server/src/tunnel/index.js.map +1 -0
- package/dist/mcp-server/src/utils/errors.d.ts +28 -0
- package/dist/mcp-server/src/utils/errors.d.ts.map +1 -0
- package/dist/mcp-server/src/utils/errors.js +66 -0
- package/dist/mcp-server/src/utils/errors.js.map +1 -0
- package/dist/mcp-server/src/utils/exec.d.ts +7 -0
- package/dist/mcp-server/src/utils/exec.d.ts.map +1 -0
- package/dist/mcp-server/src/utils/exec.js +22 -0
- package/dist/mcp-server/src/utils/exec.js.map +1 -0
- package/dist/mcp-server/src/utils/ip.d.ts +6 -0
- package/dist/mcp-server/src/utils/ip.d.ts.map +1 -0
- package/dist/mcp-server/src/utils/ip.js +33 -0
- package/dist/mcp-server/src/utils/ip.js.map +1 -0
- package/dist/mcp-server/src/utils/logger.d.ts +20 -0
- package/dist/mcp-server/src/utils/logger.d.ts.map +1 -0
- package/dist/mcp-server/src/utils/logger.js +41 -0
- package/dist/mcp-server/src/utils/logger.js.map +1 -0
- package/dist/shared/src/contractor.types.d.ts +20 -0
- package/dist/shared/src/contractor.types.d.ts.map +1 -0
- package/dist/shared/src/contractor.types.js +3 -0
- package/dist/shared/src/contractor.types.js.map +1 -0
- package/dist/shared/src/gig.types.d.ts +32 -0
- package/dist/shared/src/gig.types.d.ts.map +1 -0
- package/dist/shared/src/gig.types.js +21 -0
- package/dist/shared/src/gig.types.js.map +1 -0
- package/dist/shared/src/index.d.ts +5 -0
- package/dist/shared/src/index.d.ts.map +1 -0
- package/dist/shared/src/index.js +21 -0
- package/dist/shared/src/index.js.map +1 -0
- package/dist/shared/src/mcp-tool.types.d.ts +45 -0
- package/dist/shared/src/mcp-tool.types.d.ts.map +1 -0
- package/dist/shared/src/mcp-tool.types.js +3 -0
- package/dist/shared/src/mcp-tool.types.js.map +1 -0
- package/dist/shared/src/platform-api.types.d.ts +73 -0
- package/dist/shared/src/platform-api.types.d.ts.map +1 -0
- package/dist/shared/src/platform-api.types.js +3 -0
- package/dist/shared/src/platform-api.types.js.map +1 -0
- package/package.json +41 -0
- package/src/bin.ts +7 -0
- package/src/cli/setup.ts +317 -0
- package/src/config/defaults.ts +21 -0
- package/src/config/env.ts +74 -0
- package/src/config/index.ts +2 -0
- package/src/context/generator.ts +71 -0
- package/src/context/index.ts +6 -0
- package/src/context/templates.ts +41 -0
- package/src/git/branch.ts +46 -0
- package/src/git/diff.ts +34 -0
- package/src/git/index.ts +4 -0
- package/src/git/merge.ts +36 -0
- package/src/git/worktree.ts +42 -0
- package/src/http-wrapper.ts +94 -0
- package/src/index.ts +32 -0
- package/src/platform-client/client.ts +93 -0
- package/src/platform-client/index.ts +4 -0
- package/src/platform-client/mock-client.ts +92 -0
- package/src/platform-client/polling.ts +53 -0
- package/src/platform-client/types.ts +9 -0
- package/src/provisioning/authorized-keys.ts +52 -0
- package/src/provisioning/cleanup.ts +106 -0
- package/src/provisioning/index.ts +13 -0
- package/src/provisioning/linux-user.ts +66 -0
- package/src/provisioning/privileged.ts +128 -0
- package/src/provisioning/ssh-config.ts +197 -0
- package/src/provisioning/tmux-session.ts +136 -0
- package/src/server.ts +111 -0
- package/src/state/gig-store.ts +56 -0
- package/src/state/index.ts +3 -0
- package/src/state/persistence.ts +42 -0
- package/src/state/types.ts +14 -0
- package/src/tools/dismiss-human.ts +103 -0
- package/src/tools/index.ts +9 -0
- package/src/tools/list-humans.ts +54 -0
- package/src/tools/message-human.ts +28 -0
- package/src/tools/schemas/dismiss-human.schema.ts +21 -0
- package/src/tools/schemas/list-humans.schema.ts +6 -0
- package/src/tools/schemas/message-human.schema.ts +8 -0
- package/src/tools/schemas/summon-human.schema.ts +19 -0
- package/src/tools/summon-human.ts +180 -0
- package/src/tunnel/client.ts +116 -0
- package/src/tunnel/index.ts +26 -0
- package/src/utils/errors.ts +64 -0
- package/src/utils/exec.ts +29 -0
- package/src/utils/ip.ts +31 -0
- package/src/utils/logger.ts +55 -0
- 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,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
|
+
}
|